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

概述

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

学习数据结构的初衷

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

  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);
    if(!biTree){            //空树为完全二叉树
        return 1;
    }
    EnQueue(Q,biTree);      //将根结点入队
    while(!IsEmpty()){
        DeQueue(Q,p);       //出队
        if(p){              //出队结点非空,将其左右子树入队(注意:空结点也入队)
            EnQueue(Q,p->lchild);
            EnQueue(Q,p->rchild);
        } else{             //出队结点为空,检查其后是否有非空结点,若有,则不是完全二叉树
            while (!IsEmpty()){
                DeQueue(Q,p);
                if (p){     //结点非空,不是完全二叉树
                    return 0;
                }
            }
        }
    }
    return 1;
}
  1. 判断两棵二叉树是否相同
/*
 * 作者:xulinjie
 * 描述:判断两棵二叉树是否相同
 * 思想:递归
 */
#include <stdio.h>
#include <stdlib.h>
typedef char TElemType;
typedef struct BiTNode{
    TElemType data;
    struct BiTNode *rchild;
    struct BiTNode *lchild;
}BiTNode,BiTree;

int JudgeBitree(BiTree *biTree1,BiTree *biTree2){
    if (biTree1 == NULL && biTree2 == NULL){            //两棵树都为空,则相同
        return 1;
    } else if (biTree1 == NULL || biTree2 == NULL ||biTree1->data != biTree2->data){    //一棵为空一棵不为空,不同。 数据不同,不同
        return 0;
    } else{
        return (JudgeBitree(biTree1->lchild,biTree2->lchild)*JudgeBitree(biTree1->rchild,biTree2->rchild));//如果不同,则0*1或0*0或1*0
    }
}
  1. 非递归先序遍历二叉树(利用栈)
/*
 * 作者:xulinjie
 * 描述:非递归先序遍历
 * 思想:利用栈的思想
 * 思路:1、遇到一个结点,访问它,然后把它压栈,并去遍历它的左子树
 * 思路:2、当左子树遍历结束后,从栈顶弹出该结点,并将其指向右儿子,继续1步骤
 * 思路:3、当所有结点访问完成,即最后访问的结点为空且栈空时,停止。
 */
#include <stdio.h>
typedef char TElemType;
typedef struct BiTNode{
    TElemType data;
    struct BiTNode *rchild;
    struct BiTNode *lchild;
}BiTNode,BiTree;

int  isEmpty(){}            //判断栈是否为空
void visit(BiTree *node){}  //输出当前结点的值
void push(BiTree *node){}   //压栈
BiTree *pop(){}             //出栈

void PreOrder(BiTree *biTree){
    BiTree * node = biTree;
    while (node || (!isEmpty())){
        while (node){
            visit(node);
            push(node);
            node = node->lchild;
        }
        if(!isEmpty()){
            node = pop();
            node = node->rchild;
        }
    }
}

图相对比较难,图主要在于对相对应的算法的理解以及概念的理解,笔者会对概念做详细解释。

图的基本框架

在这里插入图片描述

图的基本概念
  1. 有向图/无向图:即有箭头方向和无箭头方向之分
  2. 无向完全图:任意两个顶点都存在边(n个顶点,n(n-1)/2条边)
    在这里插入图片描述
  3. 有向完全图:任意两个顶点都存在方向相反的两条弧(n个顶点,n(n-1)条边)
    在这里插入图片描述
  4. 子图:顶点和边都是另一个图的子集,则为子图(注意:不是所有子集都是子图,因为前提是图)
  5. 连通(针对无向图):顶点v和顶点w有路径,则连通
  6. 连通图(针对无向图):图中任意两个顶点都是连通的,则为连通图
  7. 连通分量(针对无向图):无向图中的极大连通子图
  8. 强连通图(针对有向图):有向图中任意两对顶点v,w,从v到w和从w到v都存在路径,则为强连通图
  9. 强连通分量(针对有向图):有向图中的极大强连通子图
  10. 生成树(针对连通图):含n个顶点,但只有n-1条边
    - 结论1:对于生成树而言,砍去一条边,变成非连通图
    - 结论2:加上一条边,形成一个回路
  11. 生成森林(针对非连通图):在非连通图中,连通分量的生成树构成非连通图的生成森林

  12. - 无向图的度=2e(边的2倍)
    - 有向图的度=所有顶点度之和=e
  13. 最小生成树:就是生成树带有权值的最小代价树
图的遍历

图的遍历常见有DFS和BFS,笔者就针对这两个算法讲讲他们的区别和注意点,具体实现和步骤不再赘述。

  1. 深度优先遍历:利用栈实现
    - 任意点开始都可以
    - 如果该结点与相连的所有点都已经访问过了,则回退到前一个结点,找没访问过的
    - DFS相对于树的先序遍历
    - DFS生成树高度比BFS生成树的高度:大或相等

  2. 广度优先遍历:利用队列实现
    - 任意点开始都可以
    - 同层顺序随意
    - BFS相对于树的层次遍历
    - BFS生成树高度比DFS生成树高度:小或相等

  3. 为什么DFS生成树高度比BFS生成树的高度:大或相等?
    相等情况:只有一个顶点的图
    为什么大:因为DFS是深度扩展、BFS是广度扩展。

最小生成树

最小生成树有n个顶点,n-1条边,也称最小代价树,常用Prim和Kruskal算法来求,笔者就讲讲两个算法区别和注意点,具体算法流程不再赘述。

  1. Prim算法
    - 常算稠密图
    - 不能形成环路
    - 找已相连的顶点集最小边权
    - 时间复杂度为O(v^2)与边数无关
  2. Kruskal算法
    - 常算稀疏图
    - 不能形成环路
    - 每次找最小边(所有边的最小边)
    - 时间复杂度为O(eloge)取决于边数

排序

排序重要性不必说了吧,学排序我认为必须去分类学习,下面我就按分类写相关排序算法

常考排序算法基本框架

在这里插入图片描述

基于插入思想的排序
  1. 直接插入排序

思想:将一个记录插入到已经排序好的有序表中,从而得到一个新的、记录数增1的有序表。

其中算法中有提到哨兵,其作用有两点
1、主要作用是为了防止下标遍历j越界;因为原本的for循环是需要条件j>=0,而如果将要插入元素存入A[0]中,只需要判断A[0]<A[j],因为即使有序表中的元素都比插入元素值都大,那么进行到j等于0时会自动跳出循环因为j等于0时A[0]等于A[0],这样既减少了判断条件,有防止了越界
2、还有一个作用就是防止数据丢失;因为当A[i]要插入时,会需要向后移动元素,这样就会把A[i]覆盖。当然也可以设一个临时变量,所以这个不是主要原因。

/*
 * 作者:xulinjie
 * 描述:直接插入排序
 * 思想:在一个已经有序的序列中,插入数据,重新有序。
 * 注意:0号位空出(哨兵)
 */
#include <stdio.h>
int InsertSort(int A[],int n){
    int j = 0;
    //从2开始是因为认为第一个已有序
    for (int i = 2; i <=n; ++i) {
        if (A[i]<A[i-1]){
            //哨兵,A[0]不存元素
            A[0] = A[i];
            A[i] = A[i-1];
            for (j = i-2; A[0]<A[j] ; --j) {
                A[j+1] = A[j];
            }
            A[j+1] = A[0];
        }
    }
    //输出排序后结果
    for (int k = 1; k <=n; ++k) {
        printf("%d",A[k]);
    }
}
int main() {
    int A[6] = {0,2,5,6,8,2};
    InsertSort(A,5);
    //结果:22568
    return 0;
}

  1. 折半插入排序

思想:利用二分查找法思想;
当遇到相同关键字,插在后面;
相较于直接插入排序,改善了比较次数,但不能改变移动次数;

/*
 * 作者:xulinjie
 * 描述:折半插入排序
 * 思想:折半插入(折半查找思想)。
 * 注意:相对于直接插入排序,改善了比较次数,不能改善移动次数
 */
#include <stdio.h>
void HalfSort(int A[],int n){
    int low,high,mid;
    int i,j;         //计数变量
    for (i = 2; i <= n; ++i) {
        A[0] = A[i]; //A[i]暂存到A[0]中
        low = 1;
        high = i-1;
        while (low <= high){
            mid = (low+high)/2;
            if (A[mid]<A[0]){      //查左半子表
                high = mid - 1;
            } else{
                low = mid + 1;     //查右半子表
            }
        }
        //统一后移,空出插入位置
        for (j = i-1; j>=high+1; --j){
            A[j+1] = A[j];
        }
        A[high+1] = A[0];
    }
    //结果递减
    for (int k = 1; k <= n; ++k) {
        printf("%d",A[k]);
    }
}

int main() {
    int A[6] = {0,2,5,6,8,2};
    HalfSort(A,5);
    //结果:86522
    return 0;
}

  1. 希尔排序

希尔排序与直接插入排序的区别:
1、前后记录的增量是dk,不是1
2、A[0]只是暂存单元,不是哨兵,所以要设置j>0防止下标越界

/*
 * 作者:xulinjie
 * 描述:希尔排序
 * 思想:类似于直接插入排序,但前后记录的增量是dk而不是1
 * 注意:A[0]作为暂存单元
 */
#include <stdio.h>

void ShellSort(int A[],int n){
    int i,j;                                //计数变量
    for (int dk = n/2; dk >= 1; dk=dk/2) {  //步长变化
        for (i = dk+1; i <= n ; ++i) {
            if (A[i]<A[i-dk]){              //需将A[i]插入到有序增量表中
                A[0] = A[i];                //暂存到A[0]中
                for (j = i-dk; j>0&&A[0]<A[j]; j-=dk) {
                    A[j+dk] = A[j];
                }
                A[j+dk] = A[0];
            }
        }
    }
    for (int k = 1; k <=n; ++k) {
        printf("%d",A[k]);
    }
}

int main() {
    int A[6] = {0,2,5,6,8,2};
    ShellSort(A,5);
    //结果:22568
    return 0;
}
基于交换思想的排序
  1. 冒泡排序

思想:将两个相邻的元素进行比较,大的换到右边;
思路:第一次比较第一个和第二个元素,大的换到右边;第二次比较第二个和第三个元素,大的换到右边…

  1. 快速排序

思路:首先任意选取一个记录(通常选第一个记录作为枢轴,将所有关键字较它小的记录都放置在它的位置之前,比它大的放置在它的位置之后)
注意:顺序越乱越快;顺序越有序,反而慢

/*
 * 作者:xulinjie
 * 描述:快速排序
 * 思想:1、确定枢轴 2、右与枢轴比较 3、左与枢轴比较
 * 注意:需要多趟才能完成排序
 */
#include <stdio.h>

void partition(int A[], int low , int high , int n){
    //将当前表中第一个元素设为枢轴值,对表进行划分
    int pivot = A[low];
    while (low<high){
        //枢轴值比A[high]小,没问题,high--
        while (low<high&&A[high]>=pivot){
            --high;
        }
        //当枢轴值大于high时,将high位置的元素移动到左端
        A[low] = A[high];
        //同上
        while (low<high&&A[low]<=pivot){
            ++low;
        }
        A[high] = A[low];
    }
    //枢轴元素存放到最终位置
    A[low] = pivot;

    for (int i = 0; i < n; ++i) {
        printf("%d",A[i]);
    }
}

int main() {
    int A[6] = {3,2,5,6,8,2};
    partition(A,0,5,6);
    //第一趟结果:223685
    return 0;
}
基于选择思想的排序
  1. 简单选择排序

思想:第一次把n个数中的最小的放到第一位;第二次把n-1个数中的最小的放到第二位…

/*
 * 作者:xulinjie
 * 描述:简单选择排序
 * 思想:每一趟在后面的n-i+1个中选出关键码最小的对象,作为有序序列的第i个记录
 * 注意:每一次都拿最小的插入即可,和直接插入排序的区别是直接插入排序是随机拿出一个去比较插入。
 */
#include <stdio.h>

void SelectSort(int A[],int n){
    int min = 0;//记录最小值数据下标变量
    int t;      //交换数据变量
    //n-1趟
    for (int i = 0; i < n-1; ++i) {
        min = i;                        //记录最小位置
        for (int j = i+1; j < n; ++j) { //在i~n-1中找到最小值元素
            if (A[min]>A[j]){           //更新最小元素位置
                min = j;
            }
        }
        if (min != i){                  //与第i个位置交换
          t = A[i];
          A[i] = A[min];
          A[min] = t;
        }
    }
    for (int k = 0; k < n; ++k) {
        printf("%d",A[k]);
    }
}

int main() {
    int A[6] = {0,2,5,3,8,4};
    SelectSort(A,6);
    //结果:023458
    return 0;
}
  1. 堆排序

学习堆排需要解决两个问题,解决办法比较简单,文章不再赘述

  • 如何将给定的排序记录构成一个初试堆?
  • 如何调整新堆?

判断一个数据序列是否是小根堆,可以参考以下算法:

/*
 * 作者:xulinjie
 * 描述:判断数据序列是否是小根堆
 */
#include <stdio.h>
void IsMinHeap(int A[], int n){
    //n为偶数,即有一个单分支结点
    if(n%2==0){
        //判断单分支结点
        if(A[n/2]>A[n]){
            printf("不是小根堆");
            return;
        }
        for (int i = n/2-1; i >= 1; --i) {
            if (A[i]>A[2*i] || A[i]>A[2*i+1]){
                printf("不是小根堆");
                return;
            }
        }
    }
    //n为奇数,即没有单分支结点
    else{
        for (int i = n/2; i >= 1; --i) {
            if (A[i]>A[2*i] || A[i]>A[2*i+1]){
                printf("不是小根堆");
                return;
            }
        }
    }
    printf("是小根堆");
}

int main() {
    int A[6] = {1,2,3,4,5,6};
    IsMinHeap(A,6);
    //第一趟结果:223685
    return 0;
}
基于归并思想的排序
  1. 二路归并排序

归并排序其实用的是一种分治的思想

/*
 * 作者:xulinjie
 * 描述:2路归并排序
 * 思想:分治思想
 * 注意0:表A的两段,A[low..mid],A[mid+1..high]各自有序
 * 注意1:最后两个while,只有一个会执行
 * 注意2:B只是辅助数组,B有两段,分别比较,小的进A,之后会有一段多余,直接进A
 */
#include <stdio.h>
int B[6] = {0};
void MergeSort(int A[],int low, int mid,int high){
    int j,k,h;                                              //计数变量
    for (int i = low; i <=high; ++i) {                      //将A中所有元素赋值到辅助数组B中
        B[i] = A[i];
    }
    for (j = low,k = mid+1,h = j; j<=mid&&k<=high; ++h) {   //h是归并之后数组下标计数器
        if (B[j]<=B[k]){
            A[h] = B[j++];
        } else{
            A[h] = B[k++];
        }
    }
                                                            //若第一个表未检测完,赋值
    while (j<=mid){
        A[h++] = B[j++];
    }                                                       //若第二个表未检测完,赋值
    while (k<=high){
        A[h++] = B[k++];
    }

    for (int l = 0; l < 6; ++l) {
        printf("%d",A[l]);
    }
}

int main() {
    int A[6] = {3,4,5,1,2,7};
    MergeSort(A,0,2,5);
    //第一趟结果:123457
    return 0;
}

查找

查找中一个很重要的知识点就是平均查找长度,包括查找成功/不成功,一般情况都会算等概率。

查找算法基本框架

在这里插入图片描述

顺序查找法

思想:引入哨兵,从后往前找;以下是伪码

/*
 * 作者:xulinjie
 * 描述:顺序查找
 * 思想:规定哨兵,从后往前找
 */
#include <stdio.h>
typedef struct{
    int *data;
    int tablelen;
}STable;

int search(STable st , int key){
    st.data[0] = key; //哨兵
    int i;//下标计数
    for ( i = st.tablelen; st.data[i]!=key ; --i);  //从后往前找
    return i;
}
折半查找法

折半查找的基本算法就不多赘述,比较简单,以下是折半查找的递归算法,可以参考。
说到递归,一个递归算法的成立条件必须清楚:
1、一个问题可以分解成具有相同解决思路的子问题,子子问题,换句话说这些问题都能调用同一个函数
2、必须要有终止条件

/*
 * 作者:xulinjie
 * 描述:折半查找的递归算法
 * 思想:递归思想+折半的思想
 */
#include <stdio.h>

int search(int R[],int low,int high,int key){
    if(low>high){           //递归的结束条件,也是查找的结束条件
        printf("递归结束");
        return -1;
    }
    int mid = (low+high)/2;
    if (R[mid]==key){       //查找成功
        return mid;
    } else if (key<R[mid]){ 
        return search(R,low,mid-1,key);
    } else{
        return search(R,mid+1,high,key);
    }
}
散列表查找

散列表一个很明显的特点就是:不用通过大量无效的比较就能找到关键字位置(即对散列表查找的时间复杂度为O(1),与表中元素个数无关)

  1. 思想:1、构造哈希函数;2、解决冲突

  2. 解决冲突常用有三种方法:1、线性探测再散列(不断往后找);2、二次线性探测再散列(先右1,再左1,右22,左22,右32,左32…);3、链地址法(同一个链表中,关键字自小至大有序)

  3. 填装因子 = n/m。n:关键字个数,m:表长

总结

该篇文章篇幅较长,但也没有讲到每一个知识点,讲了部分笔者认为较为重要的点,适合有一定基础的同学,作为复习通览效果更佳。笔者水平有限,如有错误,还请指正!

参考文献

《数据结构-严蔚敏版》
《大话数据结构》
https://baike.baidu.com/item/数据结构/1450#3
https://www.cnblogs.com/bigdata-stone/p/10464243.html
https://zhuanlan.zhihu.com/p/94748605

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值