【王道数据结构】第二章 | 线性表

news2024/11/18 11:30:00

目录

2.1线性表的定义

2.2线性表的基础操作

2.3顺序表的定义

2.4顺序表的基本操作

2.5 线性表的链式表示


2.1线性表的定义

线性表是具有相同数据类型的n(n>0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为:

L=(a1, a2, ... ,ai,..., an)

几个概念:

  • ai是线性表中的“第i个”元素线性表中的位序。
  • a1是表头元素;an是表尾元素。
  • 除第一个元素外,每个元素有且仅有一个直接前驱:除最后一个元素外,每个元素有且仅有一个直接后继。

2.2线性表的基础操作

  • InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
  • DestroyList(&L): 销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
  • ListInsert(&L;i,e): 插入操作。在表L中的第i个位置上插入指定元素e。
  • ListDelete(&L,i,&e): 删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
  • LocateElem(L,e): 按值查找操作。在表L中查找具有给定关键字值的元素GetElem(L,i): 按位查找操作。获取表L中第i个位置的元素的值。

其他常用操作:

  • Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
  • PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
  • Empty(L):判空操作。若L为空表,则返回true,否则返回false。

什么时候要传入参数的引用“&“-- 对参数的修改结果需要“带回来”看下面举例:

首先是传值调用:

#include<stdio.h>
void test(int x)  //形参是实参的临时拷贝
{
	x = 1024;
	printf("test函数内部 x=%d\n",x);
}
int main()
{
	int x = 1;
	printf("调用test前 x=%d\n",x);
	test(x);                       //这里的x改变了并没有传回来
	printf("调用test后 x=%d\n",x);

	return 0;
}

//输出为:
//调用test前 x=1
//test函数内部 x=1024
//调用test后 x=1
//请按任意键继续. . .

然后再看传址调用

#include<stdio.h>
void test(int &x)  //把x的地址传到函数
{
	x = 1024;
	printf("test函数内部 x=%d\n",x);
}
int main()
{
	int x = 1;
	printf("调用test前 x=%d\n",x);
	test(x);                       //这里的x通过函数传回来值改变了
	printf("调用test后 x=%d\n",x);

	return 0;
}


//输出为:
//调用test前 x=1
//test函数内部 x=1024
//调用test后 x=1024
//请按任意键继续. . .

2.3顺序表的定义

我们看完线性表的逻辑结构和基本运算,现在继续学习物理结构:顺序表 

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

【顺序表的静态分配】

顺序表的表长刚开始确定后就无法更改(存储空间是静态的)

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

#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; 
}

【顺序表的动态分配】

//顺序表的实现——动态分配
#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; 
}

【顺序表的特点】

  • 1.随机访问,即可以在 O(i) 时间内找到第i个元素。
  • 2.存储密度高,每个节点只存储数据元素
  • 3.拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
  • 4.插入、删除操作不方便,需要移动大量元素

2.4顺序表的基本操作

【插入操作】

ListInsert(&L,i,e): 插入操作。在表L中的第i个位置上插入指定元素e。

平均时间复杂度 = O(n)

#define MaxSize 10    //定义最大长度
typedef struct
{
	int data[MaxSize];  //用静态的数组存放数据
	int length;         //顺序表的当前长度
}SqList;                //顺序表的类型定义  

bool ListInsert(SqList &L, int i, int e)
{ 
    if(i<1||i>L.length+1)    //判断i的范围是否有效
        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;
}

int main()
{ 
	SqList L;   //声明一个顺序表
	InitList(L);//初始化顺序表
	//...此处省略一些代码;插入几个元素

	ListInsert(L,3,3);   //再顺序表L的第三行插入3

	return 0;
}

【删除操作】

ListDelete(&Li,&e): 删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。

平均时间复杂度 = O(n)

bool LisDelete(SqList &L, int i, int &e)
{                               
    if(i<1||i>L.length)  //判断i的范围是否有效
        return false;

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

    for(int j=L.length; j>=i; j--)
	{    
        L.data[j-1]=L.data[j];   //将第i个后的元素前移
    }
    L.length--;      //长度减1
    return true;
}
int main()
{ 
	SqList L;   //声明一个顺序表
	InitList(L);//初始化顺序表
	//...此处省略一些代码;插入几个元素
	int e = -1; //用变量e把删除的元素带回来
	if(ListDelete(L,3,e))
	{
		printf("已经删除第3个元素,删除元素值为=%d\n",e);
	}
	else
	{
		printf("为序i不合法,删除失败\n");
	}

	return 0;
}

顺序表的查找

【顺序表的按位查找】

GetElem(L,):按位查找操作。获取表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
}

 【顺序表的按值查找】

LocateElem(L,e): 按值查找操作。在表L中查找具有给定关键字值的元素

平均时间复杂度 =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;               //推出循环,说明查找失败
}
//调用LocateElem(L,9)

2.5 线性表的链式表示

 我们先学习单链表

【单链表】

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

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

【单链表的两种实现方式】

不带头节点


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;
}


带头节点

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;
}

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

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

 【单链表的基本操作】

按位序插入(带头结点)

  • Listlnsert(&Li,e): 插入操作。在表L中的第i个位置上插入指定元素e
  • 找到第i-1个结点(前驱结点),将新结点插入其后;其中头结点可以看作第0个结点,故i=1时也适用。
  • 平均时间复杂度:O(n)
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;
}


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

  • Listlnsert(&L,i,e): 插入操作。在表L中的第i个位置上插入指定元素e。将新结点插入其后;
  • 因为不带头结点,所以不存在“第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;

}

指定结点的后插操作

  • 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)
}

指定结点的前插操作

设待插入结点是s,将s插入到p的前面。我们仍然可以将s插入到*p的后面。然后将p->data与s->data交换,这样既能满足了逻辑关系,又能是的时间复杂度为O(1)

//前插操作:在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)

按位序删除节点

  • 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;
}


 【指定结点的删除

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)

单链表的查找

【单链表的按位查找】

  • GetElem(L, i): 按位查找操作,获取表L中第i个位置的元素的值;
  • 平均时间复杂度O(n)
LNode * GetElem(LinkList L, int i){
    if(i<0) return NULL;
    
    LNode *p;               //指针p指向当前扫描到的结点
    int j=0;                //当前p指向的是第几个结点
    p = L;                  //L指向头结点,头结点是第0个结点(不存数据)
    while(p!=NULL && j<i){  //循环找到第i个结点
        p = p->next;
        j++;
    }

    return p;               //返回p指针指向的值
}

【单链表的按值查找】

  • LocateElem(L, e):按值查找操作,在表L中查找具有给定关键字值的元素;
  • 平均时间复杂度:O(n)
LNode * LocateElem(LinkList L, ElemType e){
    LNode *P = L->next;    //p指向第一个结点
    //从第一个结点开始查找数据域为e的结点
    while(p!=NULL && p->data != e){
        p = p->next;
    }
    return p;           //找到后返回该结点指针,否则返回NULL
}

【求单链表的长度】

  • 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;
}

【单链表的建立】

头插法、尾插法:核心就是初始化操作、指定结点的后插操作 

 【头插法建立单链表】

  • 平均时间复杂度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;
        s->next = L->next;
        L->next = s;                         //将新结点插入表中,L为头指针
        scanf("%d", &x);   
    }
    return L;
   
}

【尾插法建立单链表】

  • 时间复杂度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;

【双链表】

双链表中节点类型的描述

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)

【循环链表】

循环单链表】
最后一个结点的指针不是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)的时间复杂度;

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

【循环双链表 】

表头结点的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);

【静态链表】

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

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

静态链表用代码表示

#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型数组;


【顺序表和链表的比较】

【逻辑结构】

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

【存储结构】

  • 顺序表:顺序存储

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

    • 优点:离散的小空间分配方便,改变容量方便
    • 缺点:不可随机存取,存储密度低

【基本操作 - 创建】

  • 顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源;
  • 静态分配:静态数组,容量不可改变
  • 动态分配:动态数组,容量可以改变,但是需要移动大量元素,时间代价高(malloc(),free())
  • 链表:只需要分配一个头结点或者只声明一个头指针

【基本操作 - 销毁】

  • 静态数组——系统自动回收空间
  • 动态分配:动态数组——需要手动free()

【基本操作-增/删】

  • 顺序表:插入/删除元素要将后续元素后移/前移;时间复杂度=O(n),时间开销主要来自于移动元素;

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

【基本操作-查】

顺序表

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

链表

  • 按位查找:O(n)
  • 按值查找:O(n)

顺序、链式、静态、动态四种存储方式的比较
顺序存储的固有特点:

  • 逻辑顺序与物理顺序一直,本质上是用数组存储线性表的各个元素(即随机存取);存储密度大,存储空间利用率高。

链式存储的固有特点:

  • 元素之间的关系采用这些元素所在的节点的“指针”信息表示(插、删不需要移动节点)。

静态存储的固有特点:

  • 在程序运行的过程中不要考虑追加内存的分配问题。

动态存储的固有特点:

  • 可动态分配内存;有效的利用内存资源,使程序具有可扩展性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/195204.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Block底层原理读书笔记-《高级编程- iOS与OS多线程和内存管理》(更新中)

1 一个Block 真正的底层都有些什么&#xff1f; Block会被解析成一个结构体&#xff08;这里成为Block结构体&#xff09;&#xff0c;这个结构体里有&#xff1a; &#xff08;1&#xff09;isa指针&#xff08;说明Block的本质是一个对象&#xff09;&#xff1a;指向Stack…

动态修改Azure DevOps区域路径或迭代路径的继承权限(Inhertiance)

Contents1. 场景描述2. 解决方案2.1 更改继承的接口&#xff1a;ChangeInhertiance2.2 获取区域路径的接口&#xff1a;ClassificationNodes1. 场景描述客户使用工作项模板&#xff0c;定制了一个设计变更流程&#xff0c;需要在不同的阶段(流程状态)控制工作项的更改权限。我们…

Grafana 系列文章(六):Grafana Explore 中的日志

&#x1f449;️URL: https://grafana.com/docs/grafana/latest/explore/logs-integration/#labels-and-detected-fields &#x1f4dd;Description: Explore 中的日志 除了指标之外&#xff0c;Explore 还允许你在以下数据源中调查你的日志。 ElasticsearchInfluxDBLoki 在基…

[算法设计与分析考点4] 改进的串匹配算法——KMP算法

算法核心思想 在BF算法的基础上加以改进&#xff0c;BF算法中每次当前字符不相等时&#xff0c;主串S要回溯到其下一个字符处&#xff0c;模式串T要回溯到 j0 位置进行下一趟的匹配。然而&#xff0c;大多数情况下&#xff0c;这种回溯是没有必要的&#xff0c;非常耗时且效率低…

qt入门介绍

1.Qt 是一个跨平台的 C开发库。主要用来开发图形用户界面&#xff08;GUI&#xff09;程序。Qt 虽然经常被当做一个 GUI 库&#xff0c;用来开发图形界面应用程序&#xff0c;但这并不是 Qt 的全部&#xff1b;Qt 除了可以绘制漂亮的界面&#xff08;包括控件、布局、交互&…

【css】前端拉下代码后,node-sass sass-loader 与node版本不符怎么办

因为每次node和sass版本匹配都容易出现问题&#xff0c; 所以如果我自己开发&#xff0c;我不使用sass或者less&#xff0c;只用原生css 但如果是拉下来的项目&#xff0c;别人已经成功使用&#xff0c;webview版本 和 sass已经匹配&#xff0c;只需考虑 node版本匹配。 根据…

Hive分析函数系列文章

Hive分析窗口函数(一) SUM,AVG,MIN,MAXhttps://blog.csdn.net/weishuai90/article/details/128843715 Hive分析窗口函数(二) NTILE,ROW_NUMBER,RANK,DENSE_RANKhttps://blog.csdn.net/weishuai90/article/details/128858824 Hive分析窗口函数(三) CUME_DIST,PERCENT_RANKhttp…

Object类有什么作用?怎样使用Object类?

在Java中提供了一个Object类&#xff0c;它是所有类的父类&#xff0c;即每个类都直接或间接继承自该类&#xff0c;因此&#xff0c;Object类通常被称之为超类、基类或根类。当定义一个类时&#xff0c;如果没有使用extends关键字为这个类显示地指定父类&#xff0c;那么该类会…

【微积分易错点总结】函数、极限和连续

&#x1f680;write in front&#x1f680; &#x1f4dc;所属专栏&#xff1a; &#x1f6f0;️博客主页&#xff1a;睿睿的博客主页 &#x1f6f0;️代码仓库&#xff1a;&#x1f389;VS2022_C语言仓库 &#x1f3a1;您的点赞、关注、收藏、评论&#xff0c;是对我最大的激励…

DSPE-PEG-DOTA,磷脂聚乙二醇大环配体 二硬脂酰基磷脂酰乙醇胺- 聚乙二醇-四氮杂环配体

中文名称&#xff1a;二硬脂酰基磷脂酰乙醇胺- 聚乙二醇-四氮杂环配体 英文名称&#xff1a;DSPE-PEG-DOTA 别称&#xff1a;1,2-distearoyl-sn-glycero-3-phosphoethanolamine-poly(ethylene glycol)-DOTA PEG分子量&#xff1a;1000、2000、3400、5000等等 用 途&#xf…

《流浪地球2》的现实倒影(一):从量子计算机到MOSS

编者按&#xff1a;跟大家一样&#xff0c;《流浪地球2》的上映让我们感到无比兴奋。作为科技领域的内容创作者&#xff0c;在《流浪地球2》中不仅看到了中国科幻电影与电影工业的崛起&#xff0c;更看到了大量现实中真实存在的科技脉络。由于这部电影在科技方面的基本功非常扎…

RK3568平台开发系列讲解(驱动基础篇)自旋锁详解

🚀返回专栏总目录 文章目录 一、什么是spinlock 自旋锁二、什么是raw_spinlock原始自旋锁三、自旋锁的使用3.1、spinlock 自旋锁相关API3.2、raw_spinlock原始自旋锁相关API沉淀、分享、成长,让自己和他人都能有所收获!😄 📢自旋锁用于处理器之间的互斥,适合保护很短的…

Python枚举类定义和使用

一些具有特殊含义的类&#xff0c;其实例化对象的个数往往是固定的&#xff0c;比如用一个类表示月份&#xff0c;则该类的实例对象最多有 12 个&#xff1b;再比如用一个类表示季节&#xff0c;则该类的实例化对象最多有 4 个。针对这种特殊的类&#xff0c;Python 3.4 中新增…

@WebServlet注解的解释和使用

大家好&#xff0c;今天分享一下WebServlet注解 首先&#xff0c;我们要知道&#xff0c;我们要写一个servlet程序,web,xml里的配置项很重要,里面包含了很多关于类绑定&#xff0c;以及服务器资源的请求路径问题&#xff08;就是注册&#xff09; 先把我这个看一遍&#xff0c…

【蓝桥杯】历届真题 完全二叉树的权值(省赛)Java

【资源限制】 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 【问题描述】 给定一棵包含N个节点的完全二叉树&#xff0c;树上每个节点都有一个权值&#xff0c;按从上到下、从左到右的顺序依…

SpringBoot将项目打包成JAR包或者WAR包

SpringBoot将项目打包成JAR包或者WAR包前言打包成JAR包打包成WAR包打包遇到的问题Maven环境问题跳过测试并打包前言 本文开发工具使用IDEA&#xff0c;已配置好Maven环境&#xff0c;如果未配置的可以参考另外一篇文章: Maven如何配置阿里镜像及安装全步骤 打包成JAR包 默认…

【C++】继承——切片、隐藏、默认成员函数、菱形

文章目录一、继承概念及定义二、赋值转换——切片三、继承的作用域——隐藏四、派生类的默认成员函数五、继承与友元六、继承与静态成员七、菱形继承及菱形虚拟继承八、继承与组合九、结语一、继承概念及定义 概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最…

刚毕业小白软考中级什么专业合适?

如果只是想考取软考中级证书&#xff0c;可以考虑比较热门的考试项目&#xff0c;当然具体还是要看和自己的职业发展和需求是否匹配为前提&#xff0c;这样对于自己今后的职业生涯规划也有一定帮助。中级科目介绍&#xff1a;软考中级备考攻略&#xff1a;反正只要认真备考&…

使用PyTorch构建卷积GAN源码(详细步骤讲解+注释版) 02人脸图片生成 上

阅读提示&#xff1a;本篇文章的代码为在普通GAN代码上实现人脸图片生成的修改&#xff0c;文章内容仅包含修改内容&#xff0c;全部代码讲解需结合下面的文章阅读。 相关资料链接为&#xff1a;使用PyTorch构建GAN生成对抗 本次训练代码使用了本地GPU计算。 1 CelebADataset类…

2023.1.23-1.29 AI行业周刊(第134期):春节回家乡的感受

春节的假期&#xff0c;时间过得很快。 年前回家在路上的时候&#xff0c;就考虑到过年时间短暂&#xff0c;但是当大年初六和家乡别离&#xff0c;和大姐二姐们再见的时候&#xff0c;心中还是充满了不舍。 2022年初回家过年的画面还印在脑海中&#xff0c;和家乡相聚的频率…