数据结构与算法的思考与历程

Posted 浅然言而信

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法的思考与历程相关的知识,希望对你有一定的参考价值。

目录

概述

笔者目前大四即将毕业,回顾大学四年,计算机的核心课程有太多太多,例如:计组、计网、程序设计、数学还有数据结构与算法这门课程,该篇博客就聊聊笔者对于数据结构与算法这门课的思考与学习历程吧!

学习数据结构的初衷

学数据结构与算法的初衷也包括学任何课程的初衷其实都是为了实际应用而准备的理论基础,不妨我们可以思考,数据结构在我们的实际应用中到底有哪些地方会用到:

  1. 编程中人人都会使用到的数组;
  2. 导航软件的路径规划、智能选路问题;
  3. 推荐系统;

一句话总结:“生活中处处都有数据结构”。

什么是算法

与其说它是“有限操作的集合”,我更愿意把它理解成“解决实际问题的方法”,算法的特性包含如下几点:

  1. 通用性:同一类问题,我们只需将它参数化,就能够一并解决。
  2. 有效性:一个完整的算法,必须保证该算法的过程和结果都有效。
  3. 确定性:例如机器人我们看上去并不知道它下一步会干什么,但是,在实现它的算法中它下一步要做什么都是确定的。
  4. 有穷性:一个算法不能死循环吧。

数据结构的分类

不管是期末、面试、考研中,数据结构的分类、按照什么结构分类,都是常见的问题。主要从逻辑和存储两个角度进行分类。

从逻辑角度分类

  1. 线性结构(栈、队列、串)
  2. 非线性结构(图、树)

从存储结构分类(又称物理结构)

  1. 顺序存储结构
  2. 链式存储结构
  3. 哈希存储结构
  4. 索引存储结构

线性表

线性表基本框架

链表与数组的区别

从逻辑结构角度

  • 静态数组:必须实现固定长度,数组可以根据下标直接存取。
  • 链表:动态的长度,方便插入删除数据项,但存取需要遍历整个链表。

从内存存储角度

  • 数组是从栈中分配空间:由编译器自动分配释放,效率高,但分配的内存容量有限。
  • 链表是从堆中分配空间:一般由程序员去分配释放,如果程序员没有释放,则由操作系统回收,动态分配,较灵活。

ps:篇幅原因就不对顺序存储多做阐述,毕竟我认为链式存储才是核心!

链表

链表其实是整个数据结构的核心,后面的树、图都与链表有关。关于链表,笔者主要聊几个核心的点以及常见的算法。

  1. 单链表的优势与劣势:单链表更适合在表尾插入,而不适合在表尾删除,为什么?

答:因为在表尾插入的操作,只需要设尾指针,使最后一个结点的next指针指向新结点即可,时间复杂度是O(1)。而假若想在尾部删除表尾元素必须知道表尾元素的前一个元素,这就导致必须从头开始遍历单链表时间复杂度是O(n)。

  1. 常见算法之链表逆置算法
/*
 * 作者:xulinjie
 * 描述:单链表逆置
 * 思路:将头结点摘下,依次从第一个结点放到头结点之后(头插)
 */
#include <stdio.h>
typedef int Elemtype;
typedef struct LNode
    Elemtype data;
    struct LNode *next;
LNode,*LinkList;

//链表逆置
LinkList reverse(LinkList L)
    LNode *p;                   //p为工作指针
    LNode *r;                   //r始终为p的后继,防止断链
    p = L->next;                //从第一个元素结点开始
    L->next = NULL;             //先将头结点L的next域置为NULL
    while (p!=NULL)            //依次将元素结点摘下
        r  = p->next;           //暂存p的后继
        p->next = L->next;      //将p结点插入到头结点之后(头插思想)
        L->next = p;            
        p = r;
    
    return L;

栈与队列

其实理解栈与队列的数据结构特点有一个不恰当但很形象的比喻(中文博大精深-_-):
栈:吃了就吐(后进先出)
队列:吃了就拉(先进先出)

栈与队列基本框架

首先不管是栈还是队列,必须明白,它们是线性表,只是加了限制的线性表。所以栈是限定仅在表尾(栈顶)进行插入和删除的线性表。栈其实在许多场景下都有应用,比如:递归就是基于栈的思想(斐波那契数列)、括号匹配等等。写一个括号匹配伪码,供参考和理解

/*
 * 作者:xulinjie
 * 描述:括号匹配(表达式由字符表示,其中可以包括3种括号:圆括号、方括号、花括号,判定给定表达式是否正确)
 * 思想:若是正括号,则入栈;若是反括号,栈空或与栈顶元素不匹配,则匹配失败  ||||| 当数组处理完后,若栈空,则匹配成功,否则失败。
 */
#include <stdio.h>
typedef char ElemType;
typedef struct

    ElemType *base; //栈底指针
    ElemType *top;  //栈顶指针
    int maxsize;  //当前可使用最大容量
sqStack;

char Pop(sqStack S);            //出栈
char Push(sqStack S, char a);   //入栈
int StackEmpty(sqStack S);      //判栈空
void InitStack(sqStack S);      //初始化栈

int matching(char *b,int n)
    //定义、初始化栈
    sqStack S;
    InitStack(S);
	//括号匹配
	//[()]是正确格式
    for (int i = 0; i < n; ++i) 
        switch(b[i])
            case ')':
                if (Pop(S)!='('||StackEmpty(S))
                    return 0;
                
                break;
            case ']':
                if (Pop(S)!='['||StackEmpty(S))
                    return 0;
                
                break;
            case '':
                if (Pop(S)!=''||StackEmpty(S))
                    return 0;
                
                break;
            case '('||'['||'':
                Push(S,b[i]);
                break;
        
    
    return StackEmpty(S);

队列

队列是在队尾插入、队头删除的特殊线性表,队列也有很多数据结构比如:循环队列、双端队列、优先队列。

  1. 循环队列:想象成圆周,它支持两种操作,队尾入队、队头出队
  2. 双端队列:它支持四种操作,队尾入队,队尾出队,队头出队和队头入队
  3. 优先队列:用堆来实现,每个元素根据优先级来出队

下面写几个考试中常考的队列基本算法伪码

  1. 反向循环队列—队尾删除、队头插入
/*
 * 作者:xulinjie
 * 描述:反向循环队列
 * 思想:对尾删除、队头插入(其实就是将原来的队头队尾互换,加变成减)
 */
#include <stdio.h>
/**
 * 队列存储结构
 */
#define maxsize 10
typedef int QElemType;
typedef struct
    QElemType *data;
    int front;
    int rear;
SqQueue;


void InitQueue(SqQueue Q)  //初始化队列
void EnQueue(SqQueue Q)    //入队
void DeQueue(SqQueue Q)    //出队
int IsEmptyQueue()              //判队空

/**
 * 入队
 * @param Q
 * @param x
 * @return
 */
int Enqueue(SqQueue Q,int x)
    if(Q.rear == (Q.front-1)%maxsize)      //队满
        return 0;
    
    Q.data[Q.front] = x;                    //队头插入
    Q.front = (Q.front - 1)%maxsize;
    return 1;


/**
 * 出队
 * @param Q
 * @param x
 * @return
 */
int Dequeue(SqQueue Q,int x)
    if(Q.rear == Q.front)                  //队空
        return 0;
    
    x = Q.data[Q.rear];
    Q.rear = (Q.rear-1)%maxsize;
    return x;


  1. 队列逆置
/*
 * 作者:xulinjie
 * 描述:队列逆置
 * 思想:1、全部元素出队,进栈。2、全部元素出栈,进队
 */
#include <stdio.h>
/**
 * 队列存储结构
 */
typedef int QElemType;
typedef struct
    QElemType *data;
    int front;
    int rear;
SqQueue;
void EnQueue(SqQueue Q,int x)    //入队
int DeQueue(SqQueue Q)    //出队
int IsEmptyQueue()         //判队空

/**
 * 栈存储结构
 */
typedef char ElemType;
typedef struct

    ElemType *base; //栈底指针
    ElemType *top;  //栈顶指针
    int maxsize;  //当前可使用最大容量
sqStack;

char Pop(sqStack S);            //出栈
char Push(sqStack S, int a);   //入栈
int StackEmpty(sqStack S);      //判栈空

void reverse(sqStack S,SqQueue Q)
    int x;
    while (!IsEmptyQueue(Q))
        x = DeQueue(Q);         //队列元素依次出队
        Push(S,x);              //依次入栈
       
    while (!StackEmpty(S))
        x = Pop(S);             //依次出栈
        EnQueue(Q,x);           //依次入队
       


  1. 层次遍历二叉树(利于队列实现)
/*
 * 作者:xulinjie
 * 描述:层次遍历二叉树
 * 思想:利于队列的思想:借助队列,先将二叉树根结点入队,然后出队输出,并判断是否有该结点的左子树,若有则入队;同理判断右子树,若有则入队,再循环出队
 */
#include <stdio.h>
/**
 * 二叉树存储结构
 */
typedef int TElemType;
typedef struct BiTNode
    TElemType data;
    struct BiTNode rchild;
    struct BiTNode lchild;
BiTNode,BiTree;
/**
 * 队列存储结构
 */
typedef int QElemType;
typedef struct
    QElemType *data;
    int front;
    int rear;
SqQueue;
void EnQueue(SqQueue Q,BiTree T)    //入队
BiTree DeQueue(SqQueue Q)           //出队
int EmptyQueue()                    //判队空
void InitQueue(SqQueue Q)           //初始化队列
void visit(BiTree T)                //打印结点的值

/***
 * 层次遍历二叉树
 * @param T 
 */
void LevelOrder(BiTree T)
    BiTree p;  //工作树 p
    SqQueue Q; //创建队列Q、并初始化
    InitQueue(Q);
    EnQueue(Q,T);//根结点入队
    while (!EmptyQueue(Q))
        p = DeQueue(Q);
        visit(p);
        if (p.lchild!=NULL)        //左子树不空,则左子树入队
            EnQueue(Q,p.lchild);
        
        if (p.rchild!=NULL)        //右子树不空,则右子树入队
            EnQueue(Q,p.rchild);
        
    


  1. 设“tag”法的循环队列
    众所周知,循环队列解决假溢出有两种实现方式:第一种就是空出一个存储空间;第二种就是利用“tag”标志,避免不必要的资源浪费,以下是利用“tag”标志法的循环队列伪码
/*
 * 作者:xulinjie
 * 描述:"tag"法循环队列
 * 思想:与原来空出一个存储空间的循环队列区别在于判满条件不再用rear+1,不用多耗一个空间,用tag标志来实现
 */
#include <stdio.h>
/**
 * 队列存储结构
 */
#define maxsize 10
typedef int QElemType;
typedef struct
    QElemType *data;
    int front;
    int rear;
    int tag;
SqQueue;

/**
 * 入队
 * @param Q
 * @param x
 */
void EnQueue(SqQueue Q,int x)
    if(Q.rear==Q.front && Q.tag==1)        //两个条件都满足才是队满
        printf("队满");
    
    Q.data[Q.rear] = x;
    Q.rear = (Q.rear+1)%maxsize;
    Q.tag = 1;                              //可能队满

/**
 * 出队
 * @param Q
 * @return
 */
int DeQueue(SqQueue Q)
    int x;
    if (Q.rear==Q.front && Q.tag==0)       //两个条件都满足则队空
        printf("队空");
    
    x = Q.data[Q.front];
    Q.front = (Q.front+1)%maxsize;
    Q.tag = 0;                              //可能队空
    return x;

聊到串,模式匹配不得不说,BF还是KMP都是重点,笔者上传两张BF和KMP算法理解的笔记。BF效率低,逐位比较。KMP效率高,利用next数组。

二叉树

二叉树可以说是数据结构非常核心地位,二叉树类型有很多,算法也有很多。

二叉树基本框架

二叉树常考的基本算法

  1. 统计二叉树的高度
/*
 * 作者:xulinjie
 * 描述:统计树的高度
 * 注意:1、树空高度为0   //2、只有根高度为1
 */
#include <stdio.h>
#include <stdlib.h>
typedef char TElemType;
typedef struct BiTNode
    TElemType data;
    struct BiTNode *rchild;
    struct BiTNode *lchild;
BiTNode,BiTree;

int GetHeight(BiTree *biTree)

    if (biTree == NULL)                                            //树空
        return 0;
     else if (biTree->lchild == NULL && biTree->rchild == NULL)   //只有根
        return 1;
     else                                                         //不是上两种
        int hl = 0;
        int hr = 0;
        hl = GetHeight(biTree->lchild);
        hr = GetHeight(biTree->rchild);
        if (hl>hr)
            return hl+1;
         else
            return hr+1;
        
    

  1. 统计叶子结点数
/*
 * 作者:xulinjie
 * 描述:统计二叉树中度为0的结点(叶子结点)
 * 思想:度为0即叶子结点
 */
#include <stdio.h>
typedef char TElemType;
typedef struct BiTNode
    TElemType data;
    struct BiTNode *rchild;
    struct BiTNode *lchild;
BiTNode,BiTree;

int NumsDegree_0(BiTree *biTree)
    if(biTree)         //非空结点
        if (biTree->lchild==NULL && biTree->rchild==NULL)
            return 1;
         else         //前往下一层
            return NumsDegree_0(biTree->lchild)+NumsDegree_0(biTree->rchild);
        
     else
        return 0;
    

  1. 交换二叉树所有结点的左右子树
/*
 * 作者:xulinjie
 * 描述:交换二叉树中所有结点的左右子树(先换后遍历)
 */
#include <stdio.h>
typedef char TElemType;
typedef struct BiTNode
    TElemType data;
    struct BiTNode *rchild;
    struct BiTNode *lchild;
BiTNode,BiTree;

void SwapTree(BiTree *biTree)
    BiTree *p;
    if (biTree!=NULL)
        //交换
        p = biTree->lchild;
        biTree->lchild = biTree->rchild;
        biTree->rchild = p;

        //遍历
        SwapTree(biTree->lchild);
        SwapTree(biTree->rchild);
     else
        return;
    

  1. 建立二叉排序树
/*
 * 作者:xulinjie
 * 描述:建立一棵二叉排序树
 * 思想:左小右大思想
 */
#include <stdio.h>
#include <stdlib.h>
typedef char TElemType;
typedef struct BiTNode
    TElemType data;
    struct BiTNode *rchild;
    struct BiTNode *lchild;
BiTNode,BiTree;

void BstInsert(BiTree *biTree,int key)
    if (biTree == NULL)
        biTree = (BiTree *)malloc(sizeof(BiTree));
        biTree->data = key;
        biTree->lchild = NULL;
        biTree->rchild = NULL;
     else if (biTree->data > key)         //左小
        BstInsert(biTree->lchild,key);
     else                                 //右大
        BstInsert(biTree->rchild,key);
    

  1. 判断给定的二叉树是否为完全二叉树
/*
 * 作者:xulinjie
 * 描述:判断给定的二叉树是否为完全二叉树
 * 思想:利用层次遍历思想
 * 思路:采用层次遍历思想,将所有结点加入队列(包括空结点)。遇空结点时,查看其后是否有非空结点,如果有,则二叉树不是完全二叉树
 */
#include <stdio.h>

/**
 * 二叉树存储结构
 */
typedef char TElemType;
typedef struct BiTNode
    TElemType data;
    struct BiTNode *rchild;
    struct BiTNode *lchild;
BiTNode,BiTree;

/**
 * 队列存储结构
 */
typedef int QElemType;
typedef struct
    QElemType *base;
    int front;
    int rear;
SqQueue;


void InitQueue(SqQueue Q)                 //初始化队列
void EnQueue(SqQueue Q,BiTree *biTree)    //入队
void DeQueue(SqQueue Q,BiTree *p)         //出队
int IsEmpty()                             //判队列是否为空

int IsComplete(BiTree *biTree)
    BiTree *p;
    SqQueue Q;
    InitQueue(Q);

以上是关于数据结构与算法的思考与历程的主要内容,如果未能解决你的问题,请参考以下文章

快速中的分割算法的解析与应用

数据结构与算法之深入解析“二叉搜索子树的最大键值和”的求解思路与算法示例

微服务架构学习与思考(12):从单体架构到微服务架构的演进历程

数据结构与算法二叉树——另一棵树的子树

第一个和最后一个枢轴元素与具有非常大 N 的通用放置

数据结构与算法 —— 二叉树