数据结构笔记——自用

请善用目录Ctrl+F

Chp1-1 数据结构的基本概念

数据概念:

image-20220601164241116

早期的计算机一一只用于处理纯数值型问题

现代计算机一一经常处理非数值型问题

对于非数值型的问题:
1.我们关心每个个体的具体信息
2.我们还关心个体之间的关系

数据项、数据元素之间的关系

数据对象

  • 数据对象是具有相同性质的数据元素的集合,是数据的一个子集。
  • 数据结构是相互之间存在一种或多种特定关系的数据元素的集合。

即,数据元素与数据结构之间没有指定的关心:

不同的数据类型可以组成相同的数据结构;相同的数据类型可以组成不同数据结构。

数据结构三要素

image-20220601165138020
  • 线性结构:

    • 逻辑结构

    • 物理结构–链式存储:

      • 优点:不会出现碎片现象
      • 缺点:每个元素因存储指针而占用额外的存储空间,且只能顺序存放
    • 物理结构–索引存储:

      • 优点:检索速度快

      • 缺点:附加的索引表额外占用存储空间,且增减和删除数据时也要修改索引表,因此会花费较多时间

    • 物理结构–散列存储:

      • 优点:检索、增加和删除节点的操作很快

      • 缺点:若散列函数不好,则可能出现元素存储单元冲突,而解决冲突会增加时间和空间的开销

  • 树形结构:

  • 图形结构:

  • 集合结构:

数据类型、抽象数据类型

数据类型是一个值的集合和定义在此集合上的一组操作的总称。
1)原子类型。其值不可再分的数据类型
2)结构类型。其值可以再分解为若干成分(分量)的数据类型

**抽象数据类型(Abstract Data Type, ADT)*是抽象数据组织及与之相关的操作。

  • ADT用数学化的语言定义数据的逻辑结构、定义运算。与具体的实现无关
  • 确定了ADT的存储结构,才 能“实现”这种数据结构

例题

03 以下属于逻辑结构的是:

A.顺序表

B.哈希表

C.有序表

D.单链表

image-20220602144549885

04 以下与数据的存储结构无关的术语是:

A.循环队列

B.链表

C.哈希表

D.栈

06 在存储数据时,通常不仅要存储各数据元素的值,而且要存储:

A.数据的操作方法

B.数据元素的类型

C.数据元素之间的关系

D.数据的存取方式

image-20220602144946879

Chp1-2 算法的基本概念

程序 = 数据结构 + 算法

算法的特性

  1. 有穷性。一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
    注:算法必须是有穷的,而程序可以是无穷的(如微信是程序不是算法)
  2. 确定性。算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出
  3. 可行性。算法中描述的操作都可以通过己经实现的基本运算执行有限次来实现。
  4. 输入。一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
  5. 输出。一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。

好算法的特性

  1. 正确性。算法应能够正确地解决求解问题。

  2. 可读性。算法应具有良好的可读性,以帮助人们理解。

  3. 健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。

  4. 高效率低存储量需求

    (时间复杂度低)+(空间复杂度低)

算法的时间复杂度

数据结构笔记——自用_第1张图片

数据结构笔记——自用_第2张图片

数据结构笔记——自用_第3张图片

结论

  • 结论1:可以只考虑阶数高的部分
  • 结论2:问题规模足够大时,常数项系数也可以忽略

运算规则

image-20220602140927553

趋向于无穷的速度::

O ( 1 ) < O ( l o g 2 n ) < O ( n ) < O ( n ∗ l o g 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) Ο(1)<Ο(log_2n)<Ο(n)<Ο(n*log_2n)<Ο(n^2)<Ο(n^3)<Ο(2^n)<Ο(n!)<Ο(n^n) O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

类型–顺序执行型:

类型–循环嵌套型:

类型–指数循环型:

类型–条件搜索型:

注:很多算法执行时间与输入的数据有关

最坏时间复杂度:最坏情况下算法的时间复杂度
平均时间复杂度:所有输入示例等概率出现的情况下,算法的期望运行时间
最好时间复杂度:最好情况下算法的时间复杂度

小节总结

例题

08 设n是描述问题规模的非负整数,下面程序片段的时间复杂度是:

x = 2;
while(x<n/2)	x = 2*x;

A. O ( l o g 2 n ) Ο(log_2n) O(log2n)

B. O ( n ) Ο(n) O(n)

C. O ( n l o g 2 n ) Ο(nlog_2n) O(nlog2n)

D. O ( n 2 ) Ο(n^2) O(n2)

[解]

执行频率最高的语句为 x = 2*x 每执行一次,x乘2。假设该语句最终执行t次,又因x初始值为2,所以有:

2 t + 1 < n / 2 2^{t+1}2t+1<n/2=>

( t + 1 ) l o g 2 2 < l o g 2 ( n / 2 ) (t+1)log_22(t+1)log22<log2(n/2)=>

( t + 1 ) < l o g 2 n − 1 (t+1)(t+1)<log2n1=>

t < l o g 2 n − 2 tt<log2n2 因此有 T ( n ) = O ( l o g 2 n ) T(n)=Ο(log_2n) T(n)=O(log2n)

10 已知两个长度分别为 m 和 n 的升序两边,若将他们合并为长度为 m + n 的一个降序链表,则最坏情况下的时间复杂度是:

A. O ( n ) Ο(n) O(n)

B. O ( m n ) Ο(mn) O(mn)

C. O ( m i n ( m , n ) ) Ο(min(m,n)) O(min(m,n))

D. O ( m a x ( m , n ) ) Ο(max(m,n)) O(max(m,n))

[解]

一开始我的想法是,先将这两个升序链表合并成一个升序链表花费 O ( m i n ( m , n ) ) Ο(min(m,n)) O(min(m,n)) ,然后再整体使用头插法实现逆序,花费 O ( m + n ) Ο(m+n) O(m+n) 两者相加: O ( m + n + m i n ( m , n ) ) Ο(m+n+min(m,n)) O(m+n+min(m,n))

  • 当 m>>n时: O ( m + n + m i n ( m , n ) ) Ο(m+n+min(m,n)) O(m+n+min(m,n))~ O ( 2 m ) Ο(2m) O(2m) ~ O ( m ) Ο(m) O(m) > > O ( n ) >>Ο(n) >>O(n)

A错;B: O ( n ) Ο(n) O(n) 故C错;而D: O ( m ) Ο(m) O(m) 满足条件

  • 当 m~n时 : O ( m + n + m i n ( m , n ) ) Ο(m+n+min(m,n)) O(m+n+min(m,n)) ~ O ( n + m ) Ο(n+m) O(n+m)~ O ( 2 n ) Ο(2n) O(2n) ~ O ( n ) Ο(n) O(n)

B~ O ( n 2 ) Ο(n^2) O(n2) 因此B错;而D: O ( n ) Ο(n) O(n) ~ O ( m ) Ο(m) O(m) 满足条件

  • 当 m< O ( m + n + m i n ( m , n ) ) Ο(m+n+min(m,n)) O(m+n+min(m,n))~ O ( 2 n ) Ο(2n) O(2n) ~ O ( n ) Ο(n) O(n)

而 D: O ( n ) Ο(n) O(n) 满足条件

  • 综上,D满足所有情况

12 下列函数的时间复杂度是:

int func(int n){
 int i = 0,sum = 0;
 while(sum<n)	sum += ++i;
 return i;
}

A. O ( l o g n ) Ο(logn) O(logn)

B. O ( n 1 / 2 ) Ο(n^{1/2}) O(n1/2)

C. O ( n ) Ο(n) O(n)

D. O ( n l o g n ) Ο(nlogn) O(nlogn)

[解]

基本运算 sum += ++i,等价于 ++i;sum = sum+i,每执行一次 i 自增1。

不难发现这是一个公差为1的等差数列求和,起始项为1,尾项为n,共有n项。可知循环次数满足: ( 1 + t ) ∗ t / 2 < n (1+t)*t/2(1+t)t/2<n => 时间复杂度为 O ( n 1 / 2 ) Ο(n^{1/2}) O(n1/2)

扩展:

求解 递归与非递归算法下的斐波那契数列算法时间复杂度
F ( n ) = { 1 , if  n = 0 , 1 ; F ( n − 1 ) + F ( n − 2 ) , if  n > 1. \begin{equation} F(n) = \begin{cases} 1, & \text{if } n = 0,1; \\ F(n-1)+F(n-2), & \text{if } n >1. \end{cases} \end{equation} F(n)={1,F(n1)+F(n2),if n=0,1;if n>1.

  • 递归求解:
int dfs(n){
   if(n<=1)	return 1;
   return dfs(n-1)+dfs(n-2);
}

执行图:

数据结构笔记——自用_第4张图片

即, 2 0 + 2 1 + 2 2 + . . + 2 n = 1 − 2 n 1 − 2 = 2 n − 1 2^0+2^1+2^2+..+2^n=\frac{1-2^n}{1-2}=2^n-1 20+21+22+..+2n=1212n=2n1 故时间复杂度为: O ( 2 n ) Ο(2^n) O(2n)

  • 非递归求解
int dfs(int n){
   if(n<=1)	return 1;
   int[] tmp = new int[n+1];//初始化数组
   tmp[1] = 1;
   for(int i = 2;i<=n;i++){
       //其过程只依靠与前两个元素
       temp[i] = temp[i-1] + temp[i-2];
   }
   return tmp[n];
}

不难看出,时间复杂度为 O ( n ) Ο(n) O(n)

空间复杂度

image-20220603163418331

Chp2-1 线性表

线性表的定义

注:数据结构三要素一一逻辑结构、数据的运算、存储结构(物理结构);储存的方式不同,运算实现的方式不同

小节回顾

chp2-2顺序表

定义

顺序表一一用顺序存储(一般使用数组)的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理
位置上也相邻
的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

而在数组分配上又可再进一步的分为动态数组分配与静态数组分配。

顺序表的特点

image-20220604180908974

小节总结

2-2.1实现–静态\动态数组分配

代码实现

#include
#include

#define MaxiSize  10    //默认顺序表的最大长度

//使用静态数组分配顺序表
/* Static Arrays*/
typedef struct SAList
{
    int data[MaxiSize];     //使用静态数组存放数据
    int length;                 //顺序表当前长度
} SASqList;                     //顺序表的定义(静态分配方式)

//使用动态分配顺序表
/* Malloc Arrays*/
typedef struct MAList
{
    int *data;                  //指示动态分配数组的指针
    int MaxSize;            //顺序表的最大容量
    int length;             //顺序表的当前长度
}MASqList;

//静态数组顺序表初始化函数
void SAInitList(SASqList *L){
    printf("<-------静态顺序表初始化函数------->\n");
    for (int i = 0; i < MaxiSize; i++)
    {
        //全部数组赋初值为0
        L->data[i] = 0;
    }
    //顺序表初始长度为0
    L->length = 0;
    printf("<-------静态顺序表初始化结束------->\n");
}

void MAInitList(MASqList *L){
    printf("<-------动态顺序表初始化函数------->\n");
    //用malloc申请一片连续的数组空间
    L->data = (int *)malloc(MaxiSize * sizeof(int));
    //顺序表初始长度为0
    L->length = 0;
    L->MaxSize = MaxiSize;
    printf("<-------静态顺序表初始化结束------->\n");
}

void MAIncreaseSize(MASqList *L,int len){
    int *p = L->data;                               //用一个指针暂存当前顺序表的数组指针
    //重新给L顺序表分配空间(变大了)
    L->data = (int *)malloc((L->MaxSize + len) * sizeof(int));
    for (int i = 0; i < L->MaxSize; i++)
    {
        L->data[i] = p[i];                          //将原来L顺序表中的元素赋值到新的区域
    }
    L->MaxSize = L->MaxSize + len;  //顺序表最大长度增加 len
    free(p);                                        //释放原来的内存空间
}

int main(){
    SASqList SAL;            //声明一个静态顺序表
    SAInitList(&SAL);       //初始化静态顺序表

    MASqList MAL;       //声明一个动态顺序表
    MAInitList(&MAL);
    return 0;
}

2-2.2顺序表的基本功能–插入

功能代码是在静态分配方式下实现的,动态分配也雷同

代码实现

/** 静态顺序表的插入 在L的位序 i 处插入元素 e
 * @param  L 需要操作的链表
 * @param i   需要插入的位置(表L中第i个位置对应于下标i-1)
 * @param e  需要插入的元素
 *  这里需要注意的是,最后e应该放到 i -1 = 2 的数组下标位置
 *  如 i = 2    j最后一进入 for 循环值为 2 则data[2] = data[1]
 *  0   1   2   3   4  5    index
 *  3   5   7   9   0       values
 *  3   5   5   7   9   0    将 下标>=1的元素全部后移一位
 *  3   e   5   7   9   0    将元素放置i-1=1处
 */
bool SAListInsert(SASqList *L,int i,int e){
    if(i<1||i>(L->length)+1)    //判断 i 的合法性
        return false;
    if(L->length>=MaxiSize) //如果空间已满则也不能插入
        return false;
    //将第 i 个元素及之后的元素后移
    for (int j = L->length; j >= i; j--)
    {
        L->data[j] = L->data[j - 1];
    }
    L->data[i - 1] = e;     //在位序 i 处放入e
    L->length++;           //长度+1
    return true;
}

时间复杂度分析

2-2.3顺序表的基本功能–删除

数据结构笔记——自用_第5张图片

代码实现

/** 静态顺序表的删除 将L的位序为 i 的元素删除 并将其赋至e
 *@param L 需要操作的表
 @param i   需要删除的位序
 @param e  对应为删除的元素带回来
 *  如 i = 2
 * 有 e = data[1] =5
 * j第一轮进入  for 循环值为 i=2                      则data[1] = data[2]
 * j最后一进入 for 循环值为 length-1=5-1=4 则data[3] = data[4]
 *  0   1   2   3   4  5    index
 *  3   5   7   9   0       values
 *  3   7   9   0   0       将 下标>=2的元素全部前移一位
 * */
bool SAListDelete(SASqList *L,int i , int *e){
    if(i<1||i>L->length)                        //判断 i 的合法性
        return false;
    *e = L->data[i-1];                          //将需要删除的元素的值赋到e
    for (int j = i; j < L->length;j++){
        L->data[j - 1] = L->data[j];    //将第 i 各位之后的元素前移
    }
    L->length--;                               //线性表长度-1
    return true;
}

时间复杂度分析

小节总结

2-2.4顺序表的查找

按位查找

代码实现
/** 获取链表L中位序为 i 的元素
 *  @param L 需要操作的链表
 *  @param i 需要取值的位序
 * */
int SAGetElem(SASqList *L,int i ){
    if(i<1||i>L->length)    //判断位序的合法性
        return -1;
    return L->data[i - 1]; //与数组的使用无异
}
时间复杂度

按值查找

代码实现
/** 按值查找    在顺序表中获取值 e 第一次出现的位序
 *  @param L 需要操作的链表
 *  @param e 需要查询的数值
 * */
int SALocateElem(SASqList *L,int e){
    for (int i = 0; i < L->data;I++){
        if(L->data[i]==e)       	//如果匹配到对应的值
            return i + 1;           //范围对应的位序
    }
    return -1;                     //如果没有查询到,则返回-1
}
时间复杂度

chp2-3链表

知识总览

image-20220702154723323

2-3.1单链表

单链表与顺序表的对比

单链表的创建

链表结构体声明

typedef struct Node
{
    int data;                       //数据域 每个节点存放一个数据
    struct Node *pNext;    //指针域 指针指向下一个节点
} LNode, *LinkList;
//NODE *    声明指向单链表的第一个节点的指针
//PNODE    声明指向单链表的第一个节点的指针--可读性更强
不带头结点与带头结点的空链表初始化
//不带头结点的空链表初始化
LinkList NHInitList(){
    return NULL;    //空表,没有数据
}

//带头结点的空链表初始化
LinkList HInitList(){
    LinkList L = NULL;
    L = (LinkList)malloc(sizeof(LNode));
    if(L=NULL){
        printf("创建失败!\n");
        exit(-1);
    }
    L->pNext = NULL;
    return L;
}
头插法创建单链表

代码实现

//带头结点的头插法创建单链表
LinkList ListCreatHeadInsert()
{
    int len;                    //所需创建的链表长度
    LNode *s;               //暂存新建节点指针
    LinkList pHead;     //返回的创建的链表头节点指针
    //创建头节点
    pHead = (LNode *)malloc(sizeof(LNode));

    if(pHead==NULL)
    {
        printf("分配内存失败!\n");
        exit(-1);
    }

    printf("<=========头插法=========>\n");
    printf("请输入需要生产的节点数:\n");
    scanf("%d", &len);
    for (int val,i = 0; i < len;i++)
    {
        printf("请输入第%d个节点的值:", i + 1);
        scanf("%d", &val);
        s = (LinkList)malloc(sizeof(LNode));  //分配空间给新节点
        if(s==NULL)
        {
            printf("分配内存失败!\n");
            exit(-1);
        }
        s->data = val;                                  //赋值给新创建的节点
        s->pNext = pHead->pNext;          //新节点接管头节点的后继节点
        pHead->pNext = s;                       //新节点成为头节点的后继节点
    }
    return pHead;                                   //返回头节点
}

重要应用:链表逆序

//利用头插法逆序带头结点的链表
LinkList Reverse(LinkList pHead){
    if(HEmpty(pHead))
        return pHead;
    LinkList p = HInitList();           //初始化空的带头结点链表
    while(!HEmpty(pHead)){
        pHead = pHead->pNext;  //链表指向下一个节点,注意,一开始进来需要跳过头节点,
        					   //因为头节点不带数据
        LNode *s = (LNode *)malloc(sizeof(LNode));//为新节点分配空间
        s->data = pHead->data;//赋值
        s->pNext = p->pNext;
        p->pNext = s;
    }
    return p;                  //返回逆序后的头节点
}
尾插法创建单链表

代码实现

// 带头节点的尾插法创建单链表
LinkList ListCreatTailInsert(){
    int len;                    //所需创建的链表长度
    LNode *s, *r;          // s:暂存新建节点指针,r保存当前的尾节点的指针
    LinkList pHead;     //返回的创建的链表头节点指针
    //创建头节点
    pHead = (LNode *)malloc(sizeof(LNode));
    r = pHead;                                          //尾指针指向pHead节点,因为是新建立的,故为最后一个节点
    if(pHead==NULL)
    {
        printf("分配内存失败!\n");
        exit(-1);
    }
    printf("<=========尾插法=========>\n");
    printf("请输入需要生产的节点数:\n");
    scanf("%d", &len);
    for (int val,i = 0; i < len;i++)
    {
        printf("请输入第%d个节点的值:", i + 1);
        scanf("%d", &val);
        s = (LinkList)malloc(sizeof(LNode));  //分配空间给新节点
        if(s==NULL)
        {
            printf("分配内存失败!\n");
            exit(-1);
        }
        s->data = val;                                  //赋值给新创建的节点
        s->pNext = NULL;                           //新节点的后继节点置空
        r->pNext = s;                                  //尾节点的后继节点指向新节点
        r = s;                                              //尾节点指向新节点
    }
    /*
        也可以将上面的语句 s->pNext = NULL 从循环中删除
        改成在循坏外最后执行:r->pNext = NULL;
    */
    return pHead;                                   //返回头节点
}

小节总结

image-20220605154923264

单链表的插入与删除

知识总览

image-20220605170604374

按位序插入(带头结点)

image-20220605171328495

代码实现

/** 带头结点的单链表按位序插入
 * @param L 操作的链
 * @param i 需要插入的位序
 * @param e 需要插入的元素
 * 头节点作为第0号节点,而插入第 i 位节点可看成插入第 i -1 节点之后
 * 故我们需要定位到 第 i - 1 节点(因为链表插入到某个节点后,需要要知道该节点的前继节点)
 * 下面定位节点的 for 循环功能每次执行完一次,指针都会指向到下一个下一个节点
 * 因此如果我们想要当最后一次执行完for循环之后能正确的指向我们所需的节点位置( 第i - 1节点)
 * 我们就得需要却确保最后一次进入for循环时遍历到的是第 i -2 个节点
 * 故下面for循环的终止条件为 j< i-1 <=> j<= i - 2
 * */
bool HListInsert(LinkList *L, int i, int e){
    if(i<i)                 //判断位序的合法性
        return false;
    LNode *p;      		   //指针p记录当前扫描到的节点
    int j = 0;            //当前p指向的是第几个节点
    p = L;               //L指向头节点,头节点是第0个节点(不存数据)
    //循环找到第 i-1 个节点
    while(p!=NULL&&j<i-1){
        p = p->pNext;
        j++;
    }

    if(p==NULL)//i值不合法
        return false;
    //分配新节点空间
    LNode *s = (LNode *)malloc(sizeof(LNode));
    s->data = e;                    //赋值给新节点
    s->pNext = p->pNext;   //将p节点看成头节点
    p->pNext = s;                //使用 头插法
    return true;
}

几种情况:

  • 插入表头: i = 0

    数据结构笔记——自用_第6张图片

  • 插入表中:0

    image-20220606173542456

  • 插入表尾:i=length

    其逻辑与插入表中类似

    image-20220606174020179

  • 插入表外:i>length

    数据结构笔记——自用_第7张图片

按位序插入(不带头节点)

image-20220607135722737

代码实现

/** 不带头结点的按序插入
 * @param L 需要操作的链表                    //L指向的是第一个节点元素指针的指针
 * @param i 需要插入的位序
 * @param e 需要插入的元素
 * 注意,由于不带头结点,插入第一位时需要修改头指针指向,所以传参数时
 * 不能值传递,只能指针传递,即传递指针的指针
 * */
bool NHListInsert(LinkList *L, int i, int e)  {
    if(i<1)
        return false;                                                           //判断位序的合法性
    //因为不带头结点,故需指要单独判断和操作。具体的,让新节点插入表头
    //并让表头指针指向该节点
    LNode *s = (LNode *)malloc(sizeof(LNode));      //给新结点分配空间
    if(i==1){
        s->data = e;                                                        //给新结点赋值
        s->pNext = (*L);                                                     //将头节点插入表头
        (*L) = s;                                                                  //将头指针的指向新节点
        return true;
    }

    /* 以下代码段同头插法的寻找第 i-1 个节点 */
    LNode *p = *(L);                                                         //指针p指向当前扫描到的节点
    int j = 1;                                                                //当前p指向的第几个几点,注意,由于是不带头节点的,故从1开始
    while(p!=NULL&&j<i-1){
        p = p->pNext;
        j++;
    }
    if(p==NULL)                                                        //i值不合法,即i大小超过当前链表长度
        return false;
    s->data = e;
    s->pNext = p->pNext;
    p->pNext = s;
    return true;
}

需要特别注意的是:i=1插入表首的情况

其他情况与带头结点一致:

指定节点的后插操作

代码实现

/** 插入到指定节点 p 后面
 * @param P 指定的节点
 *  @param e 需要插入的元素
 * */
bool HInsertNextNode(LNode *p, int e){
    if(p==NULL)
        return false;
    LNode *s = (LNode *)malloc(sizeof(LNode));
    if(s==NULL)
        return false;
    s->data = e;
    s->pNext = p->pNext;        //后插
    p->pNext = s;
    return true;
}
指定节点的前插操作

解法一:

/** 将元素 e 前插到指定节点 p 之前
 * @param L 需要菜操作的链表头指针
 * @param P 指定的节点
 * @param e 需要插入的元素
 **/
bool HInsertPriorNode(LinkList L, LNode *p, int e){
    //如果需要插入到头节点之后,即需要充当第一个节点
    if(L==p){
        LNode *s = (LNode *)malloc(sizeof(LNode));
        s->data = e;
        s->pNext = L->pNext;        //1-1使用头插法将s插到头节点L之后
        L->pNext = s;                     //1-2使用头插法将s插到头节点L之后
        return true;
    }
    LNode *r = L->pNext;                   //创建尾指针 初始化为指向头节点
    /* r指针寻找目标节点:p  ;L指针充当r的前继结点的指针   */
    while(r!=NULL){
        //如果 r 指针找到目标节点 p
        if(r==p){
            LNode *s = (LNode *)malloc(sizeof(LNode));
            s->data = e;
            s->pNext = L->pNext;                //  1-1将s节点接到L节点后面
            L->pNext = s;                             //  1-2将s节点接到L节点后面
            return true;
        }
        //如果没有找到,继续进行指针的传递
        else{
            L = r->pNext;
            r = r->pNext;
        }
    }
    //没有找到p,结束
    return false;
}

解法二:

/** 将元素 e 前插到指定节点 p 之前
 * @param   p 指定的节点
 * @param e 需要插入的元素
 * */
bool HInsertPriorNodeC(LNode *p, int e){
    if(p==NULL)
        return false;
    LNode *s = (LNode *)malloc(sizeof(LNode));
    if(s==NULL)
        return false;
    s->pNext = p->pNext;        //1.1将新节点插入到指定节点p之后
    p->pNext = s;                     //1.2将新节点插入到指定节点p之后
    s->data = p->data;            //2.1交换s与p两节点的数据
    p->data = e;                      //2.2交换s与p两节点的数据
    return true;
}

image-20220607170637272

按位序删除(带头结点/不带头结点)

代码实现

/** 带头结点删除位序为 i 的节点 并将删除的元素赋给 e
 * @param L 需要操作的链表
 * @param i 需要删除的节点位序
 * @param e 将删除的值赋给e
 * */
bool HListDelete(LinkList L, int i, int *e){
    if(i<1)                 //判断i的范围合法性
        return false;
    LNode *p;           //指针p指向的是当前扫描的节点
    p = L;                 //开始指向头节点
    int j = 0;            //L指向头节点,头结点是第0个节点,不存数据
    //循环找到第 i-1 个节点
    while(p!=NULL&&j<i-1){
        p = p->pNext;
        j++;
    }
    if(p==NULL)                             //如果 i 值不合法
        return false;
    if(p->pNext==NULL)              //第 i-1 个节点之后已无其他节点
        return false;
    LNode *q = p->pNext;          //令q指向被删除的节点
    *e = q->data;
    p->pNext = q->pNext;        //将*q节点从从链中“断开”
    free(q);                                 //释放节点的存储空间
    return true;                          //删除成功
}

不带头结点

/** 不带头结点删除位序为 i 的节点 并将删除的元素赋给 e
 *  @param L 需要操作的链表
 * @param i 需要删除的节点位序
 * @param e 将删除的值赋给e
 * */
bool NHListDelete(LinkList *L, int i, int *e){
    if(i<1||L==NULL)                 //判断 i 与 指针 的合法性
        return false;
    if(i==1){
        LNode *q = (*L);
        *e = q->data;
        *L = q->pNext;
        free(q);
        return true;
    }
    //<<=====余下操作同带头结点的====>>//
    LNode *p;           //指针p指向当前扫描的节点
    p = *L;                //开始指向首节点
    int j = 1;             //L指向首节点,首节点是第1个节点
    //循环找到第 i-1 个节点
    while(p!=NULL&&j<i-1){
        p = p->pNext;
        j++;
    }
    if(p==NULL)                             //如果 i 值不合法
        return false;
    if(p->pNext==NULL)              //第 i-1 个节点之后已无其他节点
        return false;
    LNode *q = p->pNext;          //令q指向被删除的节点
    *e = q->data;
    p->pNext = q->pNext;        //将*q节点从从链中“断开”
    free(q);                                 //释放节点的存储空间
    return true;                          //删除成功
}
指定节点的删除

方法一:传入头指针,查找所需删除节点p的前继结点

代码实现:

/** 删除指定节点 方法一:查找前继结点
 * @param L 需要操作的链表
 *  @param p 需要删除的节点
 * */
bool HDeleteNode(LinkList L, LNode *p){
    LNode *r = L->pNext;                //设置扫描尾指针,指向当前扫描到的值,初始化为第一个节点
    //查找需要删除的节点的前继结点 用L指向;此时r匹配到需要删除的节点
    while(r!=NULL&&r!=p){
        L = r;                                //1-1进行指针传递
        r = r->pNext;                         //1-2进行指针传递
    }
    if(r==NULL)
        return false;
    L->pNext = r->pNext;           			//将指针r从链表中“断开”
    free(r);
    return true;
}

方法二:与后继节点交换数据,并将后继节点删除

代码实现:

/** 删除指定节点
 * 交换p节点与其后继节点之间的数据,然后将后继节点删除
 * @param p 需要删除的节点
 * */
bool HDeleteNode1(LNode *p){
    if(p==NULL)
        return false;
    LNode *q = p->pNext;            //令q指向*p的后继节点
    p->data = p->pNext->data;   //和后继节点交换数据   注意,如果p节点为最后一个节点的话,该语句会出错
    p->pNext = q->pNext;            // 将*q节点从链中"断开"
    free(q);                                     //释放后继节点的存储空间
    return true;
}

注意,该方法不能删除最一个节点

小节总结

单链表的查询

注意,以下代码是建立在包含头节点的链表中实现的

按位查询

代码实现;

/** 删除指定节点
 * 交换p节点与其后继节点之间的数据,然后将后继节点删除
 * @param p 需要删除的节点
 * */
bool HDeleteNode1(LNode *p){
    if(p==NULL)
        return false;
    LNode *q = p->pNext;            //令q指向*p的后继节点
    p->data = p->pNext->data;   //和后继节点交换数据        注意,如果p节点为最后一个节点的话,该语句会出错
    p->pNext = q->pNext;            // 将*q节点从链中"断开"
    free(q);                                     //释放后继节点的存储空间
    return true;
}

/**  按位查找,返回第 i 个节点元素(包含头节点)
 * @param L 需要操作的链表
 *  @param i 需要返回的节点位序
 * */
LNode *HGetElem(LinkList L, int i){
    if(i<1)                             //判断位序的合法性
        return NULL;
    LNode *p = L;               //p指针指向当前扫描到的节点元素
    int j = 0;                      //p初始化为指向头节点,头节点属与第0号节点
    //循环找到第 i 个节点
    while(p!=NULL&&j<i){
        p = p->pNext;
        j++;
    }
    return p;
}
按值查询
/**  按位查找,返回第 i 个节点元素(包含头节点)
 * @param L 需要操作的链表
 *  @param i 需要返回的节点位序
 * */
LNode *HGetElem(LinkList L, int i){
    if(i<1)                             //判断位序的合法性
        return NULL;
    LNode *p = L;               //p指针指向当前扫描到的节点元素
    int j = 0;                      //p初始化为指向头节点,头节点属与第0号节点
    //循环找到第 i 个节点
    while(p!=NULL&&j<i){
        p = p->pNext;
        j++;
    }
    return p;
}

/** 按值查找,返回值=e的节点(包含头节点)
 *  @param L 需要操作的链表
 * @param e 需要查找的元素
 * */
LNode *HLocateElem(LinkList L, int e){
    LNode *p = L->pNext;
    //从第1个节点开始查找数据域为e的节点
    while(p!=NULL&&p->data!=e){
        p = p->pNext;
    }
    return p;       //      若找到则返回对应的节点,否则返回NULL
}
求表长
int Length(LinkList L){
    int len = 0;
    LNode *p = L;
    while(p->pNext!=NULL){
        len++;
        p = p->pNext;
    }
    return len;
}

2-3.5双链表

单链表vs双链表

双链表初始化

bool InitDLinkList(DLinkList *L){
    (*L) = (DNode *)malloc(sizeof(DNode));      //分配一个头结点
    if((*L) == NULL)
        return false;
    (*L)->prior = NULL;                                     //头节点的prior永远指向NULL
    (*L)->pNext = NULL;                                   //头节点之后展示还没有节点
    return true;
}

双链表的插入

image-20220701151409297

/** 在节点 p后面插入s新节点
 * @param p 需要在该节点之后插入新节点
 * @param s 需要新插入的节点
 */
bool InsertNextDNode(DNode *p, DNode *s){
    if(p==NULL||s==NULL)                    //非法参数
        return false;
    s->pNext = p->pNext;                   //①将新节点s的后指针指向p的后继节点
    s->prior = p;                          //②将新节点s的前指针指向p节点
    if(p->pNext!=NULL)
        p->pNext->prior = s;               //③将p的后继节点的前指针指向新节点s
    p->pNext = s;                          //④将p后指针指向新节点s
    return true;
}

双链表批量插入

//批量添加节点
bool Add(DLinkList *L){
    int len, i = 0;
    DNode* node = (*L);
    printf("请输入需要创建的节点个数:\n");
    scanf("%d", &len);
    while(--len>=0&&++i>0){
        int val;
        printf("请输入当前第 %d 个元素的数值:", i);
        scanf(" %d", &val);
        DNode* newNode = (DNode *)malloc(sizeof(DNode));
        if(newNode == NULL)     // 内存空间分配失败
            return false;
        newNode->data = val;
        InsertNextDNode(node, newNode);
        node = newNode;
    }
}

双链表的删除

/** 删除p节点的后继节点
 * p需要操作的节点
 */
bool DeleteNextDNode(DNode *p){
    if(p==NULL)
        return false;
    DNode *q = p->pNext;            //找到p的后继节点q
    if(q==NULL)
        return false;                          //p没有后继节点
    p->pNext = q->pNext;
    free(q);                                    //释放p的后继节点的空间
    return true;
}

双链表的遍历

数据结构笔记——自用_第8张图片

双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。时间复杂度O(n)

/** 从节点p开始遍历双链表
 *  @param p 起始节点
 * @param e 选项,1:后向遍历、2:前向遍历、3:前向遍历(跳过头节点)
 */
void traverse_DList(DNode *p,int e){
    if(p==NULL)
        return;
    switch (e)
    {
    case 1:	//后向遍历
        while(p!=NULL){
            printf("%d\n", p->data);
            p = p->pNext;
        }
        break;
    case 2://前向遍历
        while(p!=NULL){
            printf("%d\n", p->data);
            p = p->prior;
        }
        break;
    case 3: //前向遍历(跳过头节点)
        while (p->prior != NULL)
        {
            printf("%d\n", p->data);
            p = p->prior;
        }
        break;
    default:
        break;
    }
}

小节总结

2-3.6循环链表

循环单链表

定义结构体
typedef struct CNode{
    int data;                       //定义单链表节点类型
    struct CNode *pNext;    //每个节点存放一个数据元素
} CNode, *CLinkList;       //指针指向下一个节点
初始化
//初始化一个循环单链表
CLinkList InitList(){
    CLinkList L = (CLinkList)malloc(sizeof(CLinkList));       //分配一个头结点
    if(L==NULL)
        exit - 1;                                                                      //内存分配不足
    L->pNext = L;                                                               //头结点pnext指向头结点
    return L;
}
判空
//判断循环单链表是否为空
bool Empty(CLinkList L){
    if(L->pNext==L)
        return true;
    else
        return false;
}
判断节点是否为表尾节点
//判断节点p是否为循环单链表的表尾节点
bool isTail(CLinkList L ,CNode *p){
    if(p->pNext == L)
        return true;
    else
        return false;
}

循环双链表

image-20220701205524910
定义结构体
typedef struct CDNode{
    int data;
    struct CDNode *prior, *pNext;
} CDNode, *CDLinkList;
初始化循环双链表
//初始化孔的循环双链表
CDLinkList  InitCDLinkList(){
    CDNode *L = (CDNode *)malloc(sizeof(CDNode));       // 分配一个头结点
    if(L==NULL)
        exit - 1;           //内存不足,分配失败
    L->prior = L;      //头结点的prior指向头结点
    L->pNext = L;   //头结点的pNext指向头结点
    return L;
}
判空
//判断循环双链表是否为空
bool Empty(CDLinkList L){
    if(L->pNext == L)
        return true;
    else
        return false;
}
判断节点是否为表尾节点
//判断p节点是否为循环双链表的尾结点
bool isTail(CDLinkList L, CDNode *p){
    if(p->pNext == L)
        return true;
    else
        return false;
}
节点后插
//将节点 *s插入到节点 *p之后
bool InsertNextCDNode(CDNode *p, CDNode *s){
    //过程与双链表的插入类似,只是少了判断p节点后面是否为空这一步
    s->pNext = p->pNext;
    s->prior = p;
    p->pNext->prior = s;
    p->pNext = s;
}
节点删除

image-20220701211905752

//删除节点p的后继节点
bool delNextCDNode(CDNode *p){
    if(p == NULL)
        return false;
    CDNode *q = p->pNext;
    p->pNext = q->pNext;
    q->pNext->prior = p;
    free(q);
    return true;
}
小节回顾
数据结构笔记——自用_第9张图片

2-3.7静态链表

定义结构体

方法一:

struct Node{
    int data;
    int next;
};
typedef struct Node SLinkList[MaxSize];

int main(void){
    SLinkList sl;
    sl[0].data = 0;
    sl[0].next = 1;
    printf("%d\t%d", sl[0].data, sl[0].next);
    return 0;
}

方法二:

struct Node{
    int data;
    int next;
};

int main(void){
    struct Node sl[MaxSize];
    sl[0].data = 0;
    sl[0].next = 1;
    printf("%d\t%d", sl[0].data, sl[0].next);
    return 0;
}

方法三:

typedef struct {
    int data;           //存放数据元素
    int next;           //下一个元素的数组下标
} SLinkList[MaxSize];

int main(void){
    SLinkList sl;
    sl[0].data = 0;
    sl[0].next = 1;
    printf("%d\t%d", sl[0].data, sl[0].next);
    return 0;
}

基本操作

初始化
//初始化一个空的静态链表,
//初始空状态,头结点0号节点next = -1表示该节点为尾结点,其余节点 next =-2表示空闲
bool InitSList(SLinkList *sl){
    if(MaxSize<1)//最大长度不足
        return false;
    int cnt = 1;
    (*sl)[0].next = -1;
    while (cnt < MaxSize)
    {
        (*sl)[cnt].next = -2;
        cnt++;
    }
    return true;
}
判空
//判空
bool Empty(SLinkList sl){
    return MaxSize == 0 || sl[0].next == -1;
}
遍历
//遍历静态链表
void traverse_SList(SLinkList sl){
    int index = 0;
    while(index!=-1&&index != -2){  //-1:表示表尾;-2:表示空闲结点
        printf("index is : %d \t value is : %d \n", index, sl[index].data);
        index = sl[index].next;
    }
}
按位序插入
/** 插入val元素至位序为i的节点中
 * @param sl 需要操作的链表
 * @param i 需要插入的位序
 * @param vla 需要插入的节点
 */
bool Insert(SLinkList sl, int i,int val){
    if(i<0||i>MaxSize)                                      //参数非法
        return false;
    int cnt = 0;                                            // 0号头结点不存放数据
    while (sl[++cnt].next != -2);               //寻找一个空闲的节点:next = -2
    sl[cnt].data = val;                             //给新节点存入数据
    int preIndex = FindPreIndex(sl, i); //查找位序为 i-1 的结点的下标
    sl[cnt].next = sl[preIndex].next;   //修改新节点的next为 i-1 节点的next
    sl[preIndex].next = cnt;              //修改i-1 号节点的next指向新节点
}
查找前继节点
/** 查找后继节点为"位序i的节点"的next
 *  @param sl 需要操作的链表
 * @param i 需要差查找的位序
 * @return -1 :查找失败 其他数值:index对应的前继节点的下标
 */
int FindPreIndex(SLinkList sl, int i){
    if(i < 1||i>MaxSize)                        //数值非法
        return -1;
    int pindex = 0;
    int index = sl[pindex].next, cnt = 1;  //双指针查找前置节点
    while (index != -1 && cnt++ < i)
    {
        pindex = index;
        index = sl[pindex].next;
    }
    return pindex;
}
按位序删除
//删除位序为i的结点
bool Del(SLinkList sl, int i){
    if(i<1||i>MaxSize)
        return false;
    int preIndex = FindPreIndex(sl, i);                     //查找位序为 i-1 的结点的下标
    int iNext = sl[sl[preIndex].next].next;
    sl[sl[preIndex].next].next = -2;                        //置被删除的结点next = -2,表示空闲
    sl[preIndex].next = iNext; //将i-1的结点的next的修改为被删除的结点的next
    return true;
}

小节回顾

2-3.8顺序表与链表比较

  • 逻辑结构:都属于线性表,属于线性结构

  • 存储结构:

    • 顺序表:
      1. 优点:支持随机存取、存储密度高
      2. 缺点:大片连续空间分配不方便
    • 链表:
      1. 优点:离散的小空间分配方便,改变容量方便
      2. 缺点:不可随机存取,存储密度低
  • 基本操作:

    image-20220703143051445

chp3-1栈

3-1.1栈的基本概念

栈的定义

特点:后进先出(LIFO)

栈常考题型:合法的出栈顺序

小节回顾

顺序栈与链式栈的实现

顺序栈的定义

#define MaxSize 10          //定义栈中元素的最大个数

typedef struct{
    int data[MaxSize];      //静态数组存放栈中元素
    int top;                       //栈顶指针
} SqStack;

顺序栈的一些操作

初始化

//初始化栈
void InitStack(SqStack *s){
    (*s).top = -1;          //初始化栈顶指针
}

判空

//判空
bool StackEmpty(SqStack s){
    return s.top == -1;
}

入栈

//入栈
bool Push(SqStack *s, int x){
    if((*s).top==MaxSize-1)         //栈满
        return false;
    (*s).top = (*s).top + 1;         //指针先+1
    (*s).data[(*s).top] = x;        // 新元素入栈
    return true;
}

出栈

//出栈
bool Pop(SqStack(*s), int *x){
    if((*s).top==-1)        //  栈空
        return false;
    (*x) = (*s).data[(*s).top];//先出栈
    (*s).top = (*s).top - 1;    //栈顶指针-1
    return true;
}

读取栈顶元素

//读栈顶元素
bool GetTop(SqStack s, int *x){
    if(s.top==-1)
        return false;
    (*x) = s.data[s.top];
    return true;
}

顺序栈的另外一种形式:共享栈

//共享栈
typedef struct{
    int data[MaxSize];        //静态数组中存放栈中元素
    int top0;                      //0号栈顶指针
    int top1;                      //1号栈顶指针
} ShStack;

//初始化栈
void InitStack(ShStack *s){
    (*s).top0 = -1;             //初始化栈顶指针
    (*s).top1 = MaxSize;
}
//共享栈判满条件:top0+1==top1

小节回顾

image-20220712160421310

链式栈的实现

typedef struct Node{
    int data;//数据域
    struct Node *pNext;//指针域
} SNode, *LinkStack;//栈类型定义

基本操作

//初始化栈
void InitStack(LinkStack *s){
    (*s) = NULL;
}

//入栈
void Push(LinkStack *s, int x){
    if((*s)==NULL){
        printf("Is NUll Stack , will create a node\n");
        (*s) = (SNode *)malloc(sizeof(SNode));
        (*s)->data = x;
        (*s)->pNext = NULL;
    }
    else{
        SNode *p = (SNode *)malloc(sizeof(SNode));
        if(p==NULL) //内存空间不足
            exit(-1);
        p->data = x;
        p->pNext = (*s);
        (*s) = p;
    }
}

//出栈
bool Pop(LinkStack *s, int *x){
    if(s==NULL){//栈空
        printf("Is NUll Stack \n");
        return false;
    }
    SNode *p = (*s);
    (*x) = p->data;
    (*s) = p->pNext;
    free(p);
    return true;
}

//读栈顶元素
bool GetTop(LinkStack s, int *x){
    if(s==NULL){//栈空
        printf("Is NUll Stack \n");
        return false;
    }
    (*x) = s->data;
    return true;
}

//遍历栈
void Traverse(LinkStack s){
    while(s!=NULL){
        printf("%d\t", s->data);
        s = s->pNext;
    }
}

小节回顾

ch3-2队列

3-2.1队列的基本概念

小节回顾

3-2.2队列的顺序实现

顺序队列的定义

顺序队列的一些操作

//初始化队列
void InitQueue(SqQueue *Q){
    //初始化时 队头队尾指针指向0
    (*Q).front = (*Q).rear = 0;
}

//判断队列是否为空
bool QueueEmtpy(SqQueue Q){
    return Q.front == Q.rear;
}

//入队
bool EnQueue(SqQueue *Q, int x){
    if(((*Q).rear+1)%MaxSize==(*Q).front)
        return false;                       //队列满
    (*Q).data[(*Q).rear] = x;               //将x插入队尾
    (*Q).rear = ((*Q).rear + 1) % MaxSize; //队尾指针+1取模
    return true;
}

//出队 (删除一个元素并用x返回)
bool DeQueue(SqQueue *Q, int *x){
    if(QueueEmtpy(*Q))
        return false;   //队列空
    (*x) = (*Q).data[(*Q).front];
    (*Q).front = ((*Q).front + 1) % MaxSize;
    return true;
}

//取出对头元素
bool GetHead(SqQueue Q, int *x){
    if(QueueEmtpy(Q))
        return false;   //对空
    (*x) = Q.data[Q.front];
    return true;
}

//求队列长度
int LenQueue(SqQueue Q){
    return (Q.rear - Q.front + MaxSize) % MaxSize;
}

顺序队列的三种判空\满办法

方法一:

数据结构笔记——自用_第10张图片

方法二:

image-20220715152405179

方法三:

数据结构笔记——自用_第11张图片

关于顺序队列的两指针的不同定义情况

上述的关于队头队尾指针定义:队头指针front指向对头元素;队尾指针rear指向队尾元素的后一个位置。

但是还可定义:队头指针front指向对头元素;队尾指针rear指向队尾位置。

初始化时:front = 1;rear = MaxSize-1;

判空:(rear+1)%MaxSize == front;

判满:(rear+2)%MaxSize == front;

小节回顾

3-2.3队列的链式实现

链式队列的定义

链式队列的一些操作

//初始化队列(带头结点)
void HInitQueue(LinkQueue *Q){
    //初始化时 front 与 rear都指向头结点
    (*Q).front = (*Q).rear =
        (LinkNode *)malloc(sizeof(LinkNode));
    //头结点的后继节点为空
    (*Q).front->pNext = NULL;
}

//判断队列是否为空
bool HIsEmpty(LinkQueue Q){
    return Q.front == Q.rear;
}

//初始化队列(不带头节点)
void NHInitQueue(LinkQueue *Q){
    //初始化时 front 、rear 均指向NUll
    (*Q).front = (*Q).rear = NULL;
}

//判断队列是否为空(带不头结点)
bool NHIsEmpty(LinkQueue Q){
    return Q.front == NULL;
}

//入队(带头结点)
void HEnQueue(LinkQueue *Q,int x){
    LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
    s->data = x;
    s->pNext = NULL;
    (*Q).rear->pNext = s;  //新节点插入到rear之后
    (*Q).rear = s;        //修改表尾指针
}

//入队(不带头结点)
void NHEnQueue(LinkQueue *Q,int x){
    LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
    s->data = x;
    s->pNext = NULL;
    //不带头节点的队列第一个元素入队需要特殊处理
    if((*Q).front==NULL){   //在空队列中插入第一个元素
        (*Q).front = s;    //修改队头队尾指针
        (*Q).rear = s;
    }
    else{
        (*Q).rear->pNext = s;//新节点插入到 rear 节点之后
        (*Q).rear = s;      //修改 rear 指针
    }
}

//出队(带头结点)
bool HDeQueue(LinkQueue *Q, int *x){
    if(HIsEmpty(*Q))
        return false;//堆空
    LinkNode *p = (*Q).front->pNext;
    (*x) = p->data;    //用变量x返回队头元素
    (*Q).front->pNext = p->pNext;//修改头结点的 pNext 指针
    if((*Q).rear==p)  //如果要出队的结点为最后一个
        (*Q).rear = (*Q).front;//则修改队尾指针,指向头结点(因为队内没有节点了)
    free(p);
    return true;
}

//出队(不带头结点)
bool NHDeQueue(LinkQueue *Q, int *x){
    if(NHIsEmpty(*Q))
        return false;         //空队列
    LinkNode *p = (*Q).front;//P指向此次出队的结点
    (*x) = p->data;         //用变量x返回对头元素
    (*Q).front = p->pNext; //修改 front 指针
    if((*Q).rear==p){     //此次是最后一个结点出队
        (*Q).front = NULL;//front 与 rear 指针指向 NULL
        (*Q).rear = NULL;
    }
    free(p);
    return true;
}

小节总结

3-2.4双端队列

输入受限的双端队列

如何判断此种队列的序列合法性?因为只能从一端输入,故当某个点输出时,他之前的序列是已经在队内按照指定的输入顺序确定了,此时看能否通过两端的删除构造出对应的序列。

输出受限的双端队列

如何判断此种队列的序列合法性?因为只能从一端输出,故当某一个点输出时,他之前入队的序列是已经在队内确定的了。因此看能否通过两遍的输入来拼凑出对应的队列。

小节回顾

3-3.4栈的应用

括号匹配的应用

流程图

image-20220717142605302

算法实现

bool BracketCheck(char str[], int length){
    LinkStack s;
    InitStack(&s);          //初始化一个栈
    for (int i = 0; i < length;i++){
        if(str[i]=='('||str[i]=='['||str[i]=='{'){ //若为左括号 则入栈
            Push(&s, str[i] - '0');
        }
        else{
            if(StackEmpty(s))                   //若为右括号 且左括号的栈空 则返回false
                return false;
            else{
                int top;
                Pop(&s, &top);                 //获取左括号的栈顶元素
                if(str[i]==')'&&(top+'0')!='(')
                    return false;
                if(str[i]=='}'&&(top+'0')!='{')
                    return false;
                if(str[i]==']'&&(top+'0')!='[')
                    return false;
            }
        }
    }
    return StackEmpty(s);//检索完全部括号之后左括号栈为空(没有多余的左括号)这说明匹配成功
}
小节回顾

表达式求值

知识总览

中缀、后缀、前缀表达式

中缀转后缀(手算)方法
中缀转后缀表达式(机算)

后缀表达式计算(手算)
后缀表达式计算(计算机)
image-20220717153637798
中缀转前缀(手算)方式
前缀表达式计算(手算)
前缀表达式(计算机)
中缀表达式的实现

用栈实现,中缀转后缀+后缀表达式求值 两算法结合

小节回顾

递归

利用栈,将函数递归调用转变为非递归调用

3-3.5队列的应用

  1. 树的层次遍历
  2. 图的广度优先遍历
  3. OS的先来先服务

3-4.0特殊矩阵的压缩存储

知识总览

数据结构笔记——自用_第12张图片

一维数组存储

二维数组存储

image-20220719160952585

存储地址的计算

行优先:

列优先:

普通矩阵的存储

对称矩阵的压缩存储

对称矩阵定义以及压缩策略
数据结构笔记——自用_第13张图片
压缩下三角

image-20220720144120037

在按行存储且只存储了下半三角的情况利用对称性求上半三角元素

压缩策略

B数组多留出一个来存储常数C

image-20220720151316055

三对角矩阵的压缩存储

三对角矩阵性质&坐标映射

二维转一维:

image-20220720153656463

除去第一行与最后一个每行均有两个元素之外其余的每行都有三个元素:总共有 ( n − 2 ) ∗ 3 + 2 ∗ 2 = 3 n − 2 (n-2)*3+2*2=3n-2 (n2)3+22=3n2个元素 因为B数组是从0开始存储的,故B最后一个元素下标为 3 n − 3 3n-3 3n3

另外, 2 i + j − 2 2i+j-2 2i+j2是由: 3 ( i − 1 ) − 1 + j − i + 2 3(i-1)-1+j-i+2 3(i1)1+ji+2而来 ,同样的,因为B数组下标从0开始,故k要再多-1: k = 2 i + j − 3 k=2i+j-3 k=2i+j3

一维转二维:

因为B的下标是从0开始的,故第 k + 1 k+1 k+1个对应的是下标为第 k k k个,假设按照从二维转换一维的对应关系,则有:k对应的是二维数组中的下标 i,j 。又因为一行有2、3个的元素,而一个k对应的i对应某一行。因此k位于这i行全部的元素个数与前总i-1行元素个之间。 3 ( i − 1 ) − 1 < k + 1 ≤ 3 i − 1 3(i-1)-13(i1)1<k+13i1 第一个不取等号是因为第i行的元素最小值一定会大于前i-1行总元素;一定会小于等于第i行最大总元素。 ,等价于 i = ⌈ ( k + 2 ) / 3 ⌉ ⇔ ( k + 2 ) / 3 + 1 i=\lceil(k+2)/3\rceil\Leftrightarrow(k+2)/3+1 i=⌈(k+2)/3(k+2)/3+1

上面求得了i的表达式,再代入二维转一维的表达式可得余下的j :

稀疏矩阵压缩存储

失去随机存储特性

使用三元组存储
使用链式存储–十字链表、

小节回顾

ch4串

4-1.1串的定义和基本操作

串的定义

串的基本操作

小节回顾

4-2.1 朴素模式匹配算法

what

image-20220722164352260
数据结构笔记——自用_第14张图片

枚举主串 S 中的每个字符作为「发起点」,每次从原串的「发起点」和匹配串的「首位」开始尝试匹配:

匹配成功:返回本次匹配的原串「发起点」。
匹配失败:枚举原串的下一个「发起点」,重新尝试匹配。

代码实现:

王道书上

Java实现

class Solution {
    public int strStr(String ss, String pp) {
        int n = ss.length(), m = pp.length();
        char[] s = ss.toCharArray(), p = pp.toCharArray();
        // 枚举原串的「发起点」
        for (int i = 0; i <= n - m; i++) {
            // 从原串的「发起点」和匹配串的「首位」开始,尝试匹配
            int a = i, b = 0;
            while (b < m && s[a] == p[b]) {
                a++;
                b++;
            }
            // 如果能够完全匹配,返回原串的「发起点」下标
            if (b == m) return i;
        }
        return -1;
    }
}

KMP算法见附件

ch5树

5-2.1树的常考性质

  1. image-20220725153133488
  2. image-20220725154116973

小节回顾

5-2.2二叉树常考性质

一般二叉树性质

完全二叉树性质

5-2.3二叉树存储结构

二叉树的顺序存储

二叉树的链式存储

5-3.1二叉树的先中后序遍历

先序遍历

void PreTraverse(PNodeBintree P)
{
    if(P!=NULL)
    {
        visit(P);
        PreTraverse(P->LeftChild);
        PreTraverse(P->RightChild);
    }
}
//非递归版
void PreTraverse2(PNodeBintree P){
    if(!P)
        return;
    LinkStack s;
    InitStack(&s); //初始化栈
    while(P||!IsEmptyStack(s)){
        if(P){                  //节点非空
            Visit(P);           //先序访问根节点
            Push(&s, P);        //访问完,根节点入栈
            P = P->LeftChild;   //左孩子不空,一直往左走
        }
        else{
            Pop(&s, &P);        //出栈,并转向栈节点的右子树
            P = P->RightChild;  //p节点赋值
        }
    }
}

中序遍历

void InOrderTraverse(PNodeBintree P)
{
    if(P!=NULL)
    {
        PreTraverse(P->LeftChild);
        visit(P);
        PreTraverse(P->RightChild);
    }
}
//非递归版
void InOrderTraverse2(PNodeBintree P){
    if(!P)
        return;
    LinkStack s;
    InitStack(&s);
    while(P||!IsEmptyStack(s)){
        if(P){
            Push(&s, P);
            P = P->LeftChild;
        }
        else{
            Pop(&s, &P);
            Visit(P);
            P = P->RightChild;
        }
    }
}

后序遍历

void PostOrderTraverse(PNodeBintree P)
{
    if(P!=NULL)
    {
        PreTraverse(P->LeftChild);
        PreTraverse(P->RightChild);
        visit(P);
    }
}
//非递归版
void PostOrderTraverse2(PNodeBintree P){
    if(!P)
        return;
    LinkStack s;
    NodeBIntree *r = NULL;          //用来记录最后一访问的结点
    InitStack(&s);
    while(P||!IsEmptyStack(s)){
        if(P){                      //走到最左边
            Push(&s, P);
            P = P->LeftChild;
        }
        // else{ 错误
        //     Pop(&s, &P);
        //     if(!(P->RightChild))
        //         Visit(P);
        //     else{
        //         P = P->RightChild;
        //     }
        // }
        else{
            GetTop(s, &P);                          //获取栈顶元素
            if(P->RightChild&&P->RightChild!=r){   //有右子树,且并为被访问,故转向访问栈顶元素的右子树
                P = P->RightChild;
            }
            else{                                 //弹出栈顶元素并访问
                Pop(&s, &P);
                Visit(P);
                r = P;                           //记录最近访问的结点
                P = NULL;                       //因为后续遍历根节点是最后一个访问的,当访问完根节点之后,
                                                //则说明以该节点为根的树已经被访问完了
            }
        }
    }
}
void PostOrderTraverse3(PNodeBintree P){
    Stack2 st[11331];
    int top = 0;
    while(P||top!=0){
        while(P){
            st[++top].data = P;
            st[top].tag = 0;              //标记该节点左子树被访问
            P = P->LeftChild;               //一路向走,并不停入栈结点
        }
        while(top!=0&&st[top].tag==1){      //tag=1表示该节点的右子树已经被访问完了
            // st[top].tag = 0;                //将该栈节点标记恢复至初 0,因为遍历顺序先左子树
            //不需要重新给出栈的栈节点赋tag值,因为top始终指向的是形成一条路劲的结点,
            // 不会被已经出栈的栈节点tag干扰,如果新入了节点,则会在上面的语句中用0将其覆盖
            Visit(st[top--].data);          //此时左右子树均被访问完,该推出栈顶根节点并访问
        }
        if(top!=0&&st[top].tag==0){         //当前栈顶元素只被访问了左子树,此时需要访问右子树
            P = st[top].data->RightChild;   //获取栈顶结点的右子树
            st[top].tag = 1;                //标记访问了右子树
        }
    }
}

练习

小节回顾

5-3.2 二叉树的层次遍历

数据结构笔记——自用_第15张图片

5-3.3 遍历序列构造二叉树

前序+中序确定二叉树

image-20220729161514106

后序+中序确定二叉树

数据结构笔记——自用_第16张图片

层序+中序确定二叉树

image-20220729164726914

可按照先序+中序方法来做

小节回顾

小试牛刀

设计一个由先序序列与后序序列确定一棵二叉树的算法

//由先序序列和中序序列确定一颗二叉树
#include
#include
#include
 struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
 };

 struct TreeNode *PreInCreat(int *, int *, int, int, int, int);
 struct TreeNode *buildTree(int *, int, int *, int);
 void PreTraverse(struct TreeNode *);
 void InOrderTraverse(struct TreeNode *);

 int main(void){
     int preorder[5] = {3, 9, 20, 15, 7};
     int inorder[5]= {9, 3, 15, 20, 7};
     struct TreeNode *root = buildTree(preorder, 5, inorder, 5);
     printf("<-------先序遍历------->");
     PreTraverse(root);
     printf("<-------中序遍历------->");
     InOrderTraverse(root);
     return 0;
 }

 struct TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize)
 {
     return PreInCreat(preorder, inorder, 0, preorderSize - 1, 0, inorderSize - 1);
 }

/**
 * @param preorder 先序遍历序列数组
 * @param inorder  中序遍历序列数组
 * @param preorder_left 先序遍历序列左边界
 * @param preorder_right 先序遍历序列右边界
 * @param inorder_left 中序遍历序列左边界
 * @param inorder_right 中序遍历序列右边界
 */
struct TreeNode *PreInCreat(int *preorder, int *inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right){
    if (preorder_left > preorder_right) {
        return NULL;
    }
    //先序遍历的第一个节点就是根节点
    int preorder_root = preorder_left;
    //在中序遍历序列中查找根节点「preorder_root」的下标序号
    int inorder_root;
    for (inorder_root = inorder_left; inorder[inorder_root] != preorder[preorder_root]; inorder_root++);
    //创建根节点
    struct TreeNode *root = (struct TreeNode *)malloc(sizeof(struct TreeNode));
    root->val = preorder[preorder_root];
    // 得到左子树中的节点数目
    int size_left_subtree = inorder_root - inorder_left;
    // 递归地构造左子树,并连接到根节点
    // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
    root->left = PreInCreat(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
    // 递归地构造右子树,并连接到根节点
    // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
    root->right = PreInCreat(preorder, inorder, preorder_left + 1 + size_left_subtree, preorder_right, inorder_root + 1, inorder_right);
    return root;
}

5-3.4线索二叉树

中序线索化二叉树

image-20220730161147334

数据结构笔记——自用_第17张图片

先序线索化二叉树

数据结构笔记——自用_第18张图片

后序线索化二叉树

image-20220731151752622

中序线索二叉树中找中序后继

image-20220731155026633

中序线索二叉树找中序前驱

数据结构笔记——自用_第19张图片

image-20220731164440244

先序线索二叉树找后继

image-20220731171335676

先序线索二叉树找前继

达咩

数据结构笔记——自用_第20张图片

后续线索二叉树找前驱

image-20220731174431262

后续线索二叉树找后继

达咩

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dabYs0V1-1663136701592)(https://raw.githubusercontent.com/qyhc/image/master/Blog/202207311746480.png)]

小节回顾

5-4.1树的存储结构

树的逻辑结构

树的存储–双亲表示法

特性

树的存储–孩子表示法

树的存储–孩子兄弟表示法

森林和二叉树之间的转换

转换关系:”左孩子右兄弟”–最后转换成的二叉树中,父子关系在对应的森林关系中可能是兄弟关系或是原本就是父子关系

小节回顾

5-4.3树和森林的遍历

树的先根遍历

树的后根遍历

树的层次遍历

森林的先序遍历

森林的中序遍历

总结–对应关系

5-5.1哈弗曼树

定义

构建

哈夫曼编码

小节回顾

image-20220812160423033

5-5.2并查集

数据结构笔记——自用_第21张图片

存储结构

本质上为:树的双亲表示法

基本操作–初始化

并+查

数据结构笔记——自用_第22张图片
优化[并]操作

思路:

再优化

小节回顾

ch6图

6-1.1基本概念

连通图、强连通图

image-20220818151049198

子图、生成图

连通分量、强连通分量

生成树

生成森林

特殊形态的图

数据结构笔记——自用_第23张图片 image-20220814153735020

小节回顾

image-20220814153818101

6-2.1邻接矩阵

性质

6-2.2邻接表

复杂度

性质与邻接矩阵的对比

6-2.3十字链表

数据结构笔记——自用_第24张图片

删除

状态/操作
原始 image-20220815163111640
删除A-B边
删除E及关联边

小节回顾

6-2.4图的操作

操作 无向图 有向图
Adjacent(G,x,y):判断图G是否存在边或(x,y) image-20220817154836631
Neighbors(G,x):列出图G中与节点x邻接的边
InsertVertex(G,x):在途中插入顶点x 类似无向图
DeletedVertex(G,x):从图G中删除顶点x
AddEdge(G,x,y):若无向边(x,y)或有向边不存在,则向图G中添加该边 image-20220817155225314 类似无向图
RemoveEdge(G,x,y):若无向边(x,y)或有向边存在,则从图中删除该边 类似无向图
FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号;若没有则返回-1
NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1 image-20220817155517155

6-3.1图的遍历-BFS

代码

广度优先遍历序列

image-20220816165307502

复杂度分析

广度优先生成树\森林

小节回顾

6-3.2图的遍历-DFS

代码

深度优先遍历序列

复杂度分析

深度优先生成树\森林

图的遍历与连通性

image-20220817144651200

小节回顾

6-4.1最小生成树

Prim算法

image-20220818153315534
image-20220818153344889
实现思想

见ppt

image-20220818161341296

Krushal算法

顺序:1-5 6-11
实现思想

见ppt

算法比较

6-4.2最短路径

用BFS求最短路劲

代码实现

执行过程见ppt

用Dijkstra算法求最短路径

执行过程见ppt

过程与复杂度
比较Prim算法
不适用于负权值路劲

Floyd算法求最短路劲

过程见ppt

小节回顾

6-4.5有向无环图

有向无环图(DAG)—有向图+无环

转换表达式–建树:

注意:顶点不可能出现重复操作数

image-20220819154511099
转换表达式–2:化简

6-4.6_1拓扑排序

AOV网

利用拓扑排序判断有环

image-20220819175533294
代码实现
复杂度

6-4.6_2逆拓扑排序

用栈实现

用DFS实现

6-4.7关键路径

AOE网

性质

关键路径

关键时间

方法

image-20220819202221434

数据结构笔记——自用_第25张图片

数据结构笔记——自用_第26张图片

小节回顾

ch7查找

7.1基本概念

image-20220824153744676

7.2_1顺序查找

代码实现

时间复杂度

小节回顾

7.2_2折半查找

代码实现

复杂度

判定树的构造

小节回顾

7.2_3分块查找

小节回顾

7.3_1二叉排序树

查找

效率分析

小节回顾

7.3_2平衡二叉树

插入调整

效率

删除调整

数据结构笔记——自用_第27张图片

image-20220825145142633

image-20220825145344024

删除不平衡向上传导

小节回顾

7.3_4红黑树

image-20220825151401153

定义

黑高

红黑树插入

例子
  • 黑叔 LL型

  • 红叔

  • 黑叔 RR型

  • 黑叔 LR型

  • 黑叔 RL型

小节回顾

红黑树删除

7.4_1B树

定义

核心特性

树高

数据结构笔记——自用_第28张图片

image-20220826153714626

7.4_2B树插入与删除

插入

数据结构笔记——自用_第29张图片

数据结构笔记——自用_第30张图片
image-20220826160948969
image-20220826161026459

删除

情况:

  1. 处于非终端节点

    • 用直接前驱代替

      数据结构笔记——自用_第31张图片

    • 用直接后继代替

      image-20220827153334713

  2. 处于终端节点

    • 兄弟够借

      数据结构笔记——自用_第32张图片

      image-20220827153959718

    • 兄弟不够借

      image-20220827154211594

      image-20220827154245214

      数据结构笔记——自用_第33张图片

      数据结构笔记——自用_第34张图片

      合并了父节点之后关键字个数不满足B树要求<2个,故对父节点进行合并

      数据结构笔记——自用_第35张图片

      image-20220827155054043

小节回顾

7.4_3B+树

特点

数据结构笔记——自用_第36张图片

小节回顾

7_5.1散列查找

常见散列函数

除留余数法

直接定址法

image-20220827160613776

数学分析法

平方取中法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RNt06wTW-1663136701764)(https://raw.githubusercontent.com/qyhc/image/master/Blog/202208271607313.png)]

冲突处理

image-20220827161033106

拉链法

image-20220827161110470

开放定址法–线性

插入时的冲突

数据结构笔记——自用_第37张图片

查找

删除

查找效率

开放定址法–平方

开放定址法–伪随机序列

数据结构笔记——自用_第38张图片

小节回顾

chp8 排序

8_1.0排序概念

小节回顾

数据结构笔记——自用_第39张图片

你可能感兴趣的:(数据结构,数据结构)