【数据结构与算法】数据结构-王道学习笔记(一)

第一章:绪论

1.1数据结构的基本概念

**1.数据:**数据是信息的载体,是描述客观事物属性的数、字符以及所有能输入到计算机中并被程序识别和处理的符号的集合。

**2.数据元素:**数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位。例如,学生记录就是一个数据元素,它由学号、姓名、性别等数据项组成。

**3.数据对象:**数据对象是具有相同性值的数据元素的集合,是数据的一个子集。

**4.数据类型:**数据类型是一个值的集合和定义再此集合上的一组操作的总称。

1)原子类型。其值不可再分的数据类型。如bool 和int 类型。
2)结构类型。其值可以再分解为若干成分(分量)的数据类型。
3)抽象数据类型。抽象数据组织及与之相关的操作。

5.数据结构:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。

1.2数据结构的三要素

在这里插入图片描述

1.数据的逻辑结构:

在这里插入图片描述

逻辑结构是指数据元素之间的逻辑关系,即从逻辑关系上描述数据。
逻辑结构包括:

  • 集合结构:结构中的数据元素之间除“同属一个集合”外,别无其它关系。

  • 线性结构:结构中的数据元素之间只存在一对一的关系,除了第一个元素,所有元素都有唯一前驱;除了最后一个元素,所有元素都有唯一后继。

  • 树形结构:结构中数据元素之间存在一对多的关系。

  • 图状结构:数据元素之间是多对多的关系。

2.数据的存储结构(物理结构)

在这里插入图片描述

存储结构是指数据结构在计算机中的表示(又称映像),也称物理结构。
存储结构包括:

  • 顺序存储:把逻辑上相邻的元素存储在物理位置也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
  • 链式存储:逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。
  • 索引存储:在存储元素信息的同时,还建立附加的索引表,索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)
  • 散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。

3.数据的运算

施加在数据上的运算包括运算的定义何实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。

在这里插入图片描述

1.3算法的基本概念

程序=数据结构+算法
算法(algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。

算法的特性:

  • **有穷性:**一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
  • **确定性:**算法中每条指令必须有确定的含义,对于相同的输入只能得到相同的输出。
  • **可行性:**算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
  • **输入:**一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
  • **输出:**一个算法有一个多个输出,这些输出是与输入有着某种特定关系的量。

好的算法达到的目标:

  • **正确性:**算法应能够正确的求接问题。
  • **可读性:**算法应具有良好的可读性,以帮助人们理解。
  • **健壮性:**输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名奇妙地输出结果。
  • **效率与低存储量需求:**效率是指算法执行的时间,存储量需求是指算法执行过程中所需要的最大存储空间,这两者都与问题的规模有关。

在这里插入图片描述

1.4算法的时间复杂度

同一个算法不同情况下的区别:

  • 机器的性能越高执行效率越高

  • 越高级的语言执行效率越低

  • 不同的编译程序执行效率不同

一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作

T(n)=O(n),它表示随问题规模n的增大而增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度。

一般比较两个算法时间复杂度,可以只考虑阶数高的部分。

  • 加法规则:

T(n) = T1(n) +T2(n) = O(f(n)) + O(g(n)) = O(max(f(n),g(n)))

多项相加,只保留最高阶的项,且系数变为1

  • 乘法规则:

T(n) = T1(n) * T2(n) = O(f(n)) * O(g(n)) = O(f(n) * g(n))

多项相乘,都保留

!!!常见的数量级比较:

注意:数据结构中logN以什么为底?

计算时间复杂度的时候logN一般以2为底

在计算时间复杂度的时候,你会发现log的底数并不重要,底数的巨大变化并不会带来结果上的数量级变化。

1.5算法的空间复杂度

空间复杂度应考虑是否存在递归,因为递归可能会提高空间复杂度的数量级

算法的空间复杂度S(n)定义为该算法所耗费的存储空间,它是问题规模n的函数。记为S(n)=O(g(n))。

第二章:线性表

2.1线性表的定义

线性表是具有相同数据类型的n(n>0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表

2.2.1静态分配:

//顺序表的实现--静态分配

#include<stdio.h>
#define MaxSize 10   //定义表的最大长度 
typedef struct{
	int data[MaxSize];//用静态的"数组"存放数据元素
	int length; //顺序表的当前长度  
}SqList;        //顺序表的类型定义(静态分配方式) 
void InitList(SqList &L){
	 for(int i=0;i<MaxSize;i++){
	 	L.data[i]=0;  //将所有数据元素设置为默认初始值
	 }
	 L.length=0;
}
int main(){
	SqList L;//声明一个顺序表
	InitList(L);//初始化一个顺序表
	for(int i=0;i<MaxSize;i++){
		printf("data[%d]=%d\n",i,L.data[i]);
	}
	return 0; 
}

2.2.2动态分配

//顺序表的实现——动态分配
#include<stdio.h>
#include<stdlib.h>//malloc、free函数的头文件 
#define InitSize 10 //默认的最大长度
typedef struct{
	int  *data;//指示动态分配数组的指针
	int MaxSize; //顺序表的最大容量
	int length;  //顺序表的当前长度 
}SeqList; 
//初始化
void InitList(SeqList &L){
	//用malloc 函数申请一片连续的存储空间
	L.data =(int*)malloc(InitSize*sizeof(int)) ;
	L.length=0;
	L.MaxSize=InitSize;
} 
//增加动态数组的长度
void IncreaseSize(SeqList &L,int len){
	int *p=L.data;
	L.data=(int*)malloc((L.MaxSize+len)*sizeof(int));
	for(int i=0;i<L.length;i++){
		L.data[i]=p[i];   //将数据复制到新区域 
	}
	L.MaxSize=L.MaxSize+len; //顺序表最大长度增加len
	free(p);  //释放原来的内存空间 
} 
int main(void){
	SeqList L; //声明一个顺序表
	InitList(L);//初始化顺序表
	IncreaseSize(L,5);
	return 0; 
}

顺序表的特点:

优点:

  • 随机访问 ,可以在O(1)时间内找到第i个元素。

  • 存储密度高,每个节点只存储数据元素

缺点:

  • 拓展容量不方便

  • 插入、删除操作不方便,需要移动大量元素

2.2顺序表的基本操作

1.插入操作 :平均时间复杂度O(n)
bool ListInsert(SqList &L, int i, int e){ 
    //判断i的范围是否有效
    if(i<1||i>L.length+1) 
        return false;
    if(L.length>MaxSize) //当前存储空间已满,不能插入  
        return false;

    for(int j=L.length; j>i; j--){    //将第i个元素及其之后的元素后移
        L.data[j]=L.data[j-1];
    }
    L.data[i-1]=e;  //在位置i处放入e
    L.length++;      //长度加1
    return true;
}
2.删除操作:平均时间复杂度O(n)
bool LisDelete(SqList &L, int i, int &e){ // e用引用型参数 
    //判断i的范围是否有效
    if(i<1||i>L.length) 
        return false;

    e = L.data[i-1]    //将被删除的元素赋值给e

    for(int j=L.length; j>i; j--){    //将第i个后的元素前移
        L.data[j-1]=L.data[j];
    }
    L.length--;      //长度减1
    return true;
}

3.按位查找(获取L表中第i个位置的值):平均时间复杂度O(1)
#define MaxSize 10            //定义最大长度 
typedef struct{
    ElemType data[MaxSize];  //用静态的“数组”存放数据元素 
    int Length;              //顺序表的当前长度
}SqList;                     //顺序表的类型定义

ElemType GetElem(SqList L, int i){
    // ...判断i的值是否合法
    return L.data[i-1];      //注意是i-1
}
4.按值查找:平均时间复杂度O(n)
#define InitSize 10            //定义最大长度 
typedef struct{
    ElemTyp *data;  //用静态的“数组”存放数据元素 
    int Length;              //顺序表的当前长度
}SqList;   

//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SqList L, ElemType e){
    for(int i=0; i<L.lengthl i++)
        if(L.data[i] == e)  
            return i+1;     //数组下标为i的元素值等于e,返回其位序i+1
    return 0;               //推出循环,说明查找失败
}

2.3线性表的链式表示

2.3.1 单链表的定义

定义: 线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。

typedef struct LNode{//定义单链表结点类型
    ElemType data; //数据域
    struct LNode *next;//指针域
}LNode, *LinkList;

可以利用typedef关键字——数据类型重命名:type<数据类型><别名>

单链表的两种实现方式:

1、不带头结点的单链表

```bash
typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

//初始化一个空的单链表
bool InitList(LinkList &L){  //注意用引用 &
    L = NULL; //空表,暂时还没有任何结点;
    return true;
}

void test(){
    LinkList L;  //声明一个指向单链表的指针: 头指针
    //初始化一个空表
    InitList(L);
    //...
}

//判断单链表是否为空
bool Empty(LinkList L){
    if (L == NULL)
        return true;
    else
        return false;
}

**头结点:**代表链表上头指针指向的第一个结点,不带有任何数据。可以理解为数组中的下标为0的第一个元素,但是不带有任何数据

2、带头结点的单链表

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

//初始化一个单链表(带头结点)
bool InitList(LinkList &L){  
    L = (LNode*) malloc(sizeof(LNode));  //头指针指向的结点——分配一个头结点(不存储数据)
    if (L == NULL)          //内存不足,分配失败
        return false;
    L -> next = NULL;       //头结点之后暂时还没有结点
    return true;
}

void test(){
    LinkList L;  //声明一个指向单链表的指针: 头指针
    //初始化一个空表
    InitList(L);
    //...
}

//判断单链表是否为空(带头结点)
bool Empty(LinkList L){
    if (L->next == NULL)
        return true;
    else
        return false;
}

带头结点和不带头结点的比较:

**不带头结点:**写代码比较麻烦,对第一个数据节点和后续数据节点的处理需要用不同的代码逻辑,对空表和非空表的处理也需要用不同的代码逻辑;头指针指向的结点用于存放实际结果。

**带头结点:**头指针指向的头结点不存放实际数据,头结点指向的下一个结点才存放实际数据。

2.3.2单链表上基本操作的实现

1、按位序插入(带头结点):

Listinsert(&L,i,e);

在表L中的第i个位置插入指定元素e,找到第i-1的结点(i的前驱结点),将新结点插入其后;其中头结点可以看作第0个结点,故i=1时也适用。

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){  
    //判断i的合法性, i是位序号(从1开始)
    if(i<1)
        return False;
    
    LNode *p;       //指针p指向当前扫描到的结点 
    int j=0;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)

    //循环找到第i-1个结点
    while(p!=NULL && j<i-1){     //如果i>lengh, p最后会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }

    if (p==NULL)                 //i值不合法
        return false;
    
    //在第i-1个结点后插入新结点
    LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
    s->data = e;
    s->next = p->next;
    p->next = s;                 //将结点s连到p后,后两步千万不能颠倒qwq

    return true;
}

时间复杂度:O(n)

2.按位序插入(不带头结点)

ListInsert(&L, i, e):

在表L中的第i个位置上插入指定元素e = 找到第i-1个结点(i的前驱结点),将新结点插入其后; 因为不带头结点,所以不存在“第0个”结点,因此!i=1 时,需要特殊处理——插入(删除)第1个元素时,需要更改头指针L;

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

bool ListInsert(LinkList &L, int i, ElemType e){
    if(i<1)
        return false;
    
    //插入到第1个位置时的操作有所不同!
    if(i==1){
        LNode *s = (LNode *)malloc(size of(LNode));
        s->data =e;
        s->next =L;
        L=s;          //头指针指向新结点
        return true;
    }

    //i>1的情况与带头结点一样!唯一区别是j的初始值为1
    LNode *p;       //指针p指向当前扫描到的结点 
    int j=1;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)

    //循环找到第i-1个结点
    while(p!=NULL && j<i-1){     //如果i>lengh, p最后会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }

    if (p==NULL)                 //i值不合法
        return false;
    
    //在第i-1个结点后插入新结点
    LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
    s->data = e;
    s->next = p->next;
    p->next = s;          
    return true;

}

3.指定结点的后插操作:

InsertNextNode(LNode *p, ElemType e):

给定一个结点p,在其之后插入元素e; 根据单链表的链接指针只能往后查找,故给定一个结点p,那么p之后的结点我们都可知,但是p结点之前的结点无法得知;

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

bool InsertNextNode(LNode *p, ElemType e){
    if(p==NULL){
        return false;
    }

    LNode *s = (LNode *)malloc(sizeof(LNode));
    //某些情况下分配失败,比如内存不足
    if(s==NULL)
        return false;
    s->data = e;          //用结点s保存数据元素e 
    s->next = p->next;
    p->next = s;          //将结点s连到p之后

    return true;
}                         //平均时间复杂度 = O(1)

//有了后插操作,那么在第i个位置上插入指定元素e的代码可以改成:
bool ListInsert(LinkList &L, int i, ElemType e){  
    if(i<1)
        return False;
    
    LNode *p;       //指针p指向当前扫描到的结点 
    int j=0;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)

    //循环找到第i-1个结点
    while(p!=NULL && j<i-1){     //如果i>lengh, p最后4鸟会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }

    return InsertNextNode(p, e)
}

4.指定结点的前插操作

思想:设待插入结点是s,将s插入到p的前面。我们仍然可以将s插入到*p的后面。然后将p->data与s->data交换,这样既能满足了逻辑关系,又能是的时间复杂度为O(1).(另一种方法是遍历链表的时候保存两个结点的指针,Temp1,temp2,temp1是temp2的前驱结点,一旦temp2等于目标结点,既可以确定temp1是目标结点的前驱结点,这两种方法各有优劣)

//前插操作:在p结点之前插入元素e
bool InsertPriorNode(LNode *p, ElenType e){
    if(p==NULL)
        return false;
    
    LNode *s = (LNode *)malloc(sizeof(LNode));
    if(s==NULL) //内存分配失败
        return false;

    //重点来了!
    s->next = p->next;
    p->next = s;       //新结点s连到p之后
    s->data = p->data; //将p中元素复制到s
    p->data = e;       //p中元素覆盖为e

    return true;
}  //时间复杂度为O(1)

王道书代码:

bool InsertPriorNode(LNode *p, LNode *s){
    if(p==NULL || S==NULL)
        return false;
    
    s->next = p->next;
    p->next = s;  ///s连接到p
    ELemType temp = p->data;  //交换数据域部分
    p->data = s->data;
    s->data = temp;

    return true;
}

5.按位序删除节点(带头结点)

ListDelete(&L, i, &e):

删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值;头结点视为“第0个”结点;

思路:找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点;

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

bool ListDelete(LinkList &L, int i, ElenType &e){
    if(i<1) return false;

    LNode *p;       //指针p指向当前扫描到的结点 
    int j=0;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)

    //循环找到第i-1个结点
    while(p!=NULL && j<i-1){     //如果i>lengh, p最后会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }

    if(p==NULL) 
        return false;
    if(p->next == NULL) //第i-1个结点之后已无其他结点
        return false;

    LNode *q = p->next;         //令q指向被删除的结点
    e = q->data;                //用e返回被删除元素的值
    p->next = q->next;          //将*q结点从链中“断开”
    free(q)                     //释放结点的存储空间

    return true;
}

时间复杂度:O(n)

6.指定结点的删除

bool DeleteNode(LNode *p){
    if(p==NULL)
        return false;
    
    LNode *q = p->next;      //令q指向*p的后继结点
    p->data = p->next->data; //让p和后继结点交换数据域
    p->next = q->next;       //将*q结点从链中“断开”
    free(q);
    return true;
} //时间复杂度 = O(1)

2.3.3单链表的查找

按位查找

GetElem(L, i):

按位查找操作,获取表L中第i个位置的元素的值;

LNode * LocateElem(LinkList L, ElemType e){
    LNode *P = L->next;    //p指向第一个结点
    //从第一个结点开始查找数据域为e的结点
    while(p!=NULL && p->data != e){
        p = p->next;
    }
    return p;           //找到后返回该结点指针,否则返回NULL
}

2.3.4求单链表的长度

Length(LinkList L)

计算单链表中数据结点(不含头结点)的个数,需要从第一个结点看是顺序依次访问表中的每个结点。算法的时间复杂度为O(n)。

int Length(LinkList L){
    int len=0;       //统计表长
    LNode *p = L;
    while(p->next != NULL){
        p = p->next;
        len++;
    }
    return len;
}

2.3.5单链表的创建操作

1.头插法建立单链表(平均时间复杂度O(n))

思路:每次都将生成的结点插入到链表的表头。

LinkList List_HeadInsert(LinkList &L){       //逆向建立单链表
    LNode *s;
    int x;
    L = (LinkList)malloc(sizeof(LNode));     //建立头结点
    L->next = NULL;                          //初始为空链表,这步不能少!

    scanf("%d", &x);                         //输入要插入的结点的值
    while(x!=9999){                          //输入9999表结束
        s = (LNode *)malloc(sizeof(LNode));  //创建新结点
        s->data = x;
        r->next = L->next;
        L->next = s;                         //将新结点插入表中,L为头指针
        scanf("%d", &x);   
    }
    return L;
   
}

2.尾插法建立单链表(时间复杂度O(n))

思路:每次将新节点插入到当前链表的表尾,所以必须增加一个尾指针r,使其始终指向当前链表的尾结点。好处:生成的链表中结点的次序和输入数据的顺序会一致。

LinkList List_TailInsert(LinkList &L){       //正向建立单链表
    int x;                                   //设ElemType为整型int
    L = (LinkList)malloc(sizeof(LNode));     //建立头结点(初始化空表)
    LNode *s, *r = L;                        //r为表尾指针
    scanf("%d", &x);                         //输入要插入的结点的值
    while(x!=9999){                          //输入9999表结束
        s = (LNode *)malloc(sizeof(LNode));
        s->data = x;
        r->next = s;
        r = s                                //r指针指向新的表尾结点
        scanf("%d", &x);   
    }
    r->next = NULL;                          //尾结点指针置空
    return L;
}

链表的逆置:

算法思想:逆置链表初始为空,原表中结点从原链表中依次“删除”,再逐个插入逆置链表的表头(即“头插”到逆置链表中),使它成为逆置链表的“新”的第一个结点,如此循环,直至原链表为空;

LNode *Inverse(LNode *L)
{
	LNode *p, *q;
	p = L->next;     //p指针指向第一个结点
	L->next = NULL;  //头结点指向NULL

	while (p != NULL){
		q = p;
		p = p->next;
		q->next = L->next;  
		L->next = q;
	}
	return L;
}

2.3.6双链表

双链表中节点类型的描述:

typedef struct DNode{            //定义双链表结点类型
    ElemType data;               //数据域
    struct DNode *prior, *next;  //前驱和后继指针
}DNode, *DLinklist;

双链表的初始化(带头结点)

typedef struct DNode{            //定义双链表结点类型
    ElemType data;               //数据域
    struct DNode *prior, *next;  //前驱和后继指针
}DNode, *DLinklist;

//初始化双链表
bool InitDLinkList(Dlinklist &L){
    L = (DNode *)malloc(sizeof(DNode));      //分配一个头结点
    if(L==NULL)                              //内存不足,分配失败
        return false;
    
    L->prior = NULL;   //头结点的prior指针永远指向NULL
    L->next = NULL;    //头结点之后暂时还没有结点
    return true;
}

void testDLinkList(){
    //初始化双链表
    DLinklist L;         // 定义指向头结点的指针L
    InitDLinkList(L);    //申请一片空间用于存放头结点,指针L指向这个头结点
    //...
}

//判断双链表是否为空
bool Empty(DLinklist L){
    if(L->next == NULL)    //判断头结点的next指针是否为空
        return true;
    else
        return false;
}

双链表的插入操作

后插操作:InsertNextDNode(p, s):

在p结点后插入s结点

bool InsertNextDNode(DNode *p, DNode *s){ //将结点 *s 插入到结点 *p之后
    if(p==NULL || s==NULL) //非法参数
        return false;
    
    s->next = p->next;
    if (p->next != NULL)   //p不是最后一个结点=p有后继结点  
        p->next->prior = s;
    s->prior = p;
    p->next = s;
    
    return true;
}

按位序插入操作:
思路:从头结点开始,找到某个位序的前驱结点,对该前驱结点执行后插操作;
前插操作:
思路:找到给定结点的前驱结点,再对该前驱结点执行后插操作;
双链表的删除操作
删除p节点的后继节点

//删除p结点的后继结点
bool DeletNextDNode(DNode *p){
    if(p==NULL) return false;
    DNode *q =p->next;            //找到p的后继结点q
    if(q==NULL) return false;     //p没有后继结点;
    p->next = q->next;
    if(q->next != NULL)           //q结点不是最后一个结点
        q->next->prior=p;
    free(q);

    return true;
}

//销毁一个双链表
bool DestoryList(DLinklist &L){
    //循环释放各个数据结点
    while(L->next != NULL){
        DeletNextDNode(L);  //删除头结点的后继结点
    free(L); //释放头结点
    L=NULL;  //头指针指向NULL

    }
}

双链表的遍历操作
前向遍历

while(p!=NULL){
    //对结点p做相应处理,eg打印
    p = p->prior;
}

后向遍历

while(p!=NULL){
    //对结点p做相应处理,eg打印
    p = p->next;
}

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

2.3.7循环链表

1.循环单链表
最后一个结点的指针不是NULL,而是指向头结点

typedef struct LNode{            
    ElemType data;               
    struct LNode *next;  
}DNode, *Linklist;

/初始化一个循环单链表
bool InitList(LinkList &L){
    L = (LNode *)malloc(sizeof(LNode)); //分配一个头结点
    if(L==NULL)             //内存不足,分配失败
        return false;
    L->next = L;            //头结点next指针指向头结点
    return true;
}

//判断循环单链表是否为空(终止条件为p或p->next是否等于头指针)
bool Empty(LinkList L){
    if(L->next == L)
        return true;    //为空
    else
        return false;
}

//判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L, LNode *p){
    if(p->next == L)
        return true;
    else
        return false;
}

单链表和循环单链表的比较:
**单链表:**从一个结点出发只能找到该结点后续的各个结点;对链表的操作大多都在头部或者尾部;设立头指针,从头结点找到尾部的时间复杂度=O(n),即对表尾进行操作需要O(n)的时间复杂度;
**循环单链表:**从一个结点出发,可以找到其他任何一个结点;设立尾指针,从尾部找到头部的时间复杂度为O(1),即对表头和表尾进行操作都只需要O(1)的时间复杂度;

==优点:==从表中任一节点出发均可找到表中其他结点。

2.循环双链表
表头结点的prior指向表尾结点,表尾结点的next指向头结点

typedef struct DNode{          
    ElemType data;               
    struct DNode *prior, *next;  
}DNode, *DLinklist;

//初始化空的循环双链表
bool InitDLinkList(DLinklist &L){
    L = (DNode *) malloc(sizeof(DNode));    //分配一个头结点
    if(L==NULL)            //内存不足,分配失败
        return false;  
    L->prior = L;          //头结点的prior指向头结点
    L->next = L;           //头结点的next指向头结点
}

void testDLinkList(){
    //初始化循环单链表
    DLinklist L;
    InitDLinkList(L);
    //...
}

//判断循环双链表是否为空
bool Empty(DLinklist L){
    if(L->next == L)
        return true;
    else
        return false;
}

//判断结点p是否为循环双链表的表尾结点
bool isTail(DLinklist L, DNode *p){
    if(p->next == L)
        return true;
    else
        return false;
}

双链表的插入(循环双链表):

bool InsertNextDNode(DNode *p, DNode *s){ 
    s->next = p->next;
    p->next->prior = s;
    s->prior = p;
    p->next = s;
}

双链表的删除

//删除p的后继结点q
p->next = q->next;
q->next->prior = p;
free(q);

双向循环链表:

和单链的循环表类似,双向链表也可以有循环表,让头结点的前驱指针指向链表的最后一个结点,让最后一个结点的后继指针指向头结点。

结构定义:

typedef struct DuLNode{
	Elemtype data;
	struct DulNode *prior,*next;
	
} DuLNode,*DuLinkList;

2.3.8静态链表

1、定义:

**单链表:**各个结点散落在内存中的各个角落,每个结点有指向下一个节点的指针(下一个结点在内存中的地址);

**静态链表:**用数组的方式来描述线性表的链式存储结构: 分配一整片连续的内存空间,各个结点集中安置,包括了——数据元素and下一个结点的数组下标(游标)

  • 其中数组下标为0的结点充当"头结点"
  • 游标为-1表示已经到达表尾
  • 若每个数据元素为4B,每个游标为4B,则每个结点共8B;假设起始地址为addr,则数据下标为2的存
  • 地址为:addr+8*2
  • 注意: 数组下标——物理顺序,位序——逻辑顺序;
  • 优点:增、删操作不需要大量移动元素;
  • 缺点:不能随机存取,只能从头结点开始依次往后查找,容量固定不变!

2.静态链表用代码表示:

#define MaxSize 10        //静态链表的最大长度

struct Node{              //静态链表结构类型的定义
    ElemType data;        //存储数据元素
    int next;             //下一个元素的数组下标(游标)
};

//用数组定义多个连续存放的结点
void testSLinkList(){
    struct Node a[MaxSize];  //数组a作为静态链表, 每一个数组元素的类型都是struct Node
    //...
}

也可以这样:

#define MaxSize 10        //静态链表的最大长度

typedef struct{           //静态链表结构类型的定义
    ELemType data;        //存储数据元素
    int next;             //下一个元素的数组下标
}SLinkList[MaxSize];

void testSLinkList(){
    SLinkList a;
}

也等同于:

#define MaxSize 10        //静态链表的最大长度

struct Node{              //静态链表结构类型的定义
    ElemType data;        //存储数据元素
    int next;             //下一个元素的数组下标(游标)
};

typedef struct Node SLinkList[MaxSize]; //重命名struct Node,用SLinkList定义“一个长度为MaxSize的Node型数组;

**注意:**SLinkList a 强调a是静态链表;struct Node a 强调a是一个Node型数组;

3.静态链表基本操作的实现

  • 初始化静态链表:把a[0]的next设为-1

  • 查找某个位序(不是数组下标,位序是各个结点在逻辑上的顺序)的结点:从头结点出发挨个往后遍历结点,时间复杂度O=(n)

  • 在位序为i上插入结点:① 找到一个空的结点,存入数据元素;② 从头结点出发找到位序为i-1的结点;③修改新结点的next;④ 修改i-1号结点的next;

  • 删除某个结点:① 从头结点出发找到前驱结点;② 修改前驱节点的游标;③ 被删除节点next设为-2;

2.3.9 顺序表和链表的比较

1.逻辑结构

  • 顺序表和链表都属于线性表,都是线性结构

2.存储结构

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

3. 基本操作 - 创建

  • 顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源;

  • 静态分配:静态数组,容量不可改变

  • 动态分配:动态数组,容量可以改变,但是需要移动大量元素,时间代价高(malloc(),free())

  • 链表:只需要分配一个头结点或者只声明一个头指针

4. 基本操作 - 销毁

  • 顺序表:修改 Length = 0
    • 静态数组——系统自动回收空间
typedef struct{
    ElemType *data;
    int MaxSize;
    int length;
}SeqList; 
  • 动态分配:动态数组——需要手动free()
//创
L.data = (ELemType *)malloc(sizeof(ElemType) *InitSize)
//销
free(L.data);

//!malloc() 和 free() 必须成对出现

5.基本操作-增/删

  • 顺序表:插入/删除元素要将后续元素后移/前移;时间复杂度=O(n),时间开销主要来自于移动元素;
  • 链表:插入/删除元素只需要修改指针;时间复杂度=O(n),时间开销主要来自查找目标元素

6.基本操作-查

  • 顺序表
    • 按位查找:O(1)
  • 按值查找:O(n),若表内元素有序,可在O(log2n)时间内找到
  • 链表
    • 按位查找:O(n)
    • 按值查找:O(n)

2.3.10顺序、链式、静态、动态四种存储方式的比较

1、顺序存储的固有特点:
逻辑顺序与物理顺序一直,本质上是用数组存储线性表的各个元素(即随机存取);存储密度大,存储空 间利用率高。
2、链式存储的固有特点:
元素之间的关系采用这些元素所在的节点的“指针”信息表示(插、删不需要移动节点)。
3、静态存储的固有特点:
在程序运行的过程中不要考虑追加内存的分配问题。
4、动态存储的固有特点:
可动态分配内存;有效的利用内存资源,使程序具有可扩展性。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值