第2章 线性表

线性表是一种常见的数据结构,具有以下特点:

  1. 数据元素有限:线性表中的数据元素数量是有限的,可以是空表,也可以包含多个元素。

  2. 顺序:线性表中的元素是按照一定的顺序排列的,每个元素都有一个确定的位置(称为“位序”),通常从1开始编号。

  3. 类型相同:线性表中的所有元素属于同一数据类型,例如整数、字符或自定义结构体等。

  4. 抽象性:线性表是一种抽象的数据结构,定义了数据的逻辑关系,而不关心具体的存储方式。线性表可以通过顺序存储(如数组)或链式存储(如链表)来实现。

  5. 每个元素是单个元素:线性表中的每个元素都是独立的个体,不包含子结构。例如,一个整数线性表中的每个元素都是一个整数,而不是一个数组或另一个线性表。

线性表的顺序表示

静态分配

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

#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 < L.length; i++) {                //顺序表的打印
		printf("data[%d]=%d\n", i, L.data[i]);
	}
	return 0;
}

 插入

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

bool ListInsert(SqList &L, int i, int e) {
	if (i<1 || i>L.length + 1) {    //判断i的范围是否有效
		printf("范围无效\n"); return false;
	}
	if (L.length >= MaxSize) { //当前存储空间已满,不能插入  
		printf("储空间已满\n"); 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;
}

// 打印顺序表中的元素
void PrintList(SqList &L) {
	printf("顺序表元素: ");

	for (int i = 0; i < L.length; i++) {
		printf("%d ", L.data[i]);
	}
	printf("\n");
}

int main() {
	SqList L;   //声明一个顺序表
	InitList(L);//初始化顺序表
	//...此处省略一些代码;插入几个元素
	ListInsert(L, 1, 3);   //再顺序表L的第三行插入3
	PrintList(L);
	return 0;
}

删除

// 删除顺序表i位置的数据并存入e
bool ListDelete(SqList &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++) //将第i个位置后的元素前移 
		L.data[j-1] = L.data[j];
	L.length--;
	return true; 
}

按位查找

// 静态分配的按位查找
#define MaxSize 10
 
typedef struct {
	ElemType data[MaxSize]; 
	int length;
}SqList;
 
ElemType GetElem(SqList L, int i) {
	return L.data[i-1];
}

按值查找

#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)

动态分配

#include <stdio.h>
#include <stdlib.h>

#define InitSize 5 // 默认的最大长度

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

// 初始化顺序表
void InitList(SeqList &L) {
	L.data = (int *)malloc(InitSize * sizeof(int));
	//if (L.data == NULL) {
	//	printf("内存分配失败\n");
	//	exit(1);
	//}
	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));
	//if (L.data == NULL) {
	//	printf("内存分配失败\n");
	//	exit(1);
	//}
	for (int i = 0; i < L.length; i++) {
		L.data[i] = p[i];
	}
	L.MaxSize = L.MaxSize + len;
	free(p);
}

// 在顺序表的指定位置插入元素
int ListInsert(SeqList &L, int index, int element) {
	if (index < 1 || index > L.length + 1) {
		printf("插入位置不合法\n");
		return 0;
	}
	if (L.length >= L.MaxSize) {
		printf("顺序表已满,正在扩容...\n");
		IncreaseSize(L, 5); // 扩容5个元素
	}
	for (int i = L.length; i >= index; i--) {
		L.data[i] = L.data[i - 1];
	}
	L.data[index - 1] = element;
	L.length++;
	return 1;
}

// 打印顺序表中的元素
void PrintList(SeqList &L) {
	printf("顺序表元素: ");
	for (int i = 0; i < L.length; i++) {
		printf("%d ", L.data[i]);
	}
	printf("\n");
}

int main() {
	SeqList L;
	InitList(L); // 初始化顺序表

	// 往顺序表中插入元素
	ListInsert(L, 1, 10);
	ListInsert(L, 2, 20);
	ListInsert(L, 3, 30);
	ListInsert(L, 4, 40);
	ListInsert(L, 5, 50);
	ListInsert(L, 6, 60); // 触发扩容
	ListInsert(L, 7, 70);
	ListInsert(L, 8, 80);
	ListInsert(L, 9, 90);
	ListInsert(L, 10, 100);
	ListInsert(L, 11, 110); // 再次触发扩容

	// 打印顺序表
	PrintList(L);

	// 释放动态分配的内存
	free(L.data);
	return 0;
}

 线性表的链式表示

        单链表:用链式存储实现了线性结构。一个结点存储一个数据元素,各结点间的前后关系用一个指针表示。
特点:

  • 优点:不要求大片连续空间,改变容量方便。
  • 缺点:不可随机存取,要耗费一定空间存放指针。

两种实现方式:
        带头结点,写代码更方便。头结点不存储数据,头结点指向的下一个结点才存放实际数据。
        不带头结点,麻烦。对第一个数据结点与后续数据结点的处理需要用不同的代码逻辑,对空表和非空表的处理需要用不同的代码逻辑。

struct LNode {
    ElemType data;       // 数据域,存放一个数据元素
    struct LNode *next;  // 指针域,指向下一个结点
};

typedef struct LNode LNode;     // 定义 LNode 为 struct LNode 的别名
typedef struct LNode *LinkList; // 定义 LinkList 为 struct LNode* 的别名

//可以直接
typedef struct LNode {
    ElemType data;       // 数据域,存放一个数据元素
    struct LNode *next;  // 指针域,指向下一个结点
}LNode,*LinkList;
  • 原本需要写 struct LNode,现在可以直接写 LNode

  • 原本需要写 struct LNode *,现在可以直接写 LinkList

  • 强调这是一个单链表--使用 LinkList
  • 强调这是一个结点--使用 LNode* 

不带头初始化

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){
    return (L==NULL)
}

带头初始化

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)

假设位置3图示:

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)                 //如果p指针知道最后再往后就是NULL
        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)
}
 

指定结点的前插操作

如何实现方法1:传入头指针

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

一句话:就是在p后插入s,然后交换数值p和s的数值,相对位置就是s在p前。

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

单链表的删除

按位序删除节点

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

 书上代码:

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;
 
bool ListDelete(LinkList &L, int i, ElenType &e){
 
    LNode *p;       //指针p指向当前扫描到的结点 
    int j=0;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)
 
    //循环找到第i-1个结点
    while(p->next!=NULL && j<i-1){     //如果i>lengh, p最后会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }
 
    if(p->next == NULL || j>i-1) //第i-1个结点之后已无其他结点
        return false;
 
    LNode *q = p->next;         //令q指向被删除的结点
    e = q->data;                //用e返回被删除元素的值
    p->next = q->next;          //将*q结点从链中“断开”
    free(q)                     //释放结点的存储空间
 
    return true;
}
 
 

如果不带头结点,删除第1个元素,是否需要特殊处理?

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

    LNode *p;
    int j = 0;

    // 特殊情况:删除第1个元素
    if (i == 1) {
        if (L == NULL) return false; // 链表为空
        LNode *q = L;               // q指向第1个结点
        e = q->data;                // 用e返回被删除元素的值
        L = L->next;                // 修改头指针L,指向第2个结点
        free(q);                    // 释放第1个结点的内存
        return true;
    }

    p = L; // p指向头结点

    // 循环找到第i-1个结点
    while (p != NULL && j < i - 1) {
        p = p->next;
        j++;
    }

    if (p == NULL || 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(1)

缺点:如果p是最后个结点,只能从表头开始依次寻找p的前驱,时间复杂度O(n)

类似前x,删除p节点,找到p后一个节点q,然后讲p=q,就是被覆盖了,然后再删除q。

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)
 

 单链表的查找

按位查找

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指针指向的值
}
 

按值查找

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

单链表的建立

尾插法建立单链表

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

// 使用尾插法建立单链表L
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;
}

头插法建立单链表

这个可以做链表逆置

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

链表的逆置

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

就是一个头断开,然后再次头插法

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

后插操作

排除最后一个节点的潜在错误,直接指向p

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

这个1、4不能颠倒

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

前向遍历跳过头节点:

        从某个节点开始,向前遍历链表,但不处理头节点。因为这个头的前置节点是null

while (p-> prior != NULL)
    //对结点p做相应处理
    p = p->prior;

后向遍历

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

循环单链表

最后一个结点的指针不是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;
}
特性单链表循环单链表
定义每个节点包含数据和指向下一个节点的指针,最后一个节点指向 NULL每个节点包含数据和指向下一个节点的指针,最后一个节点指向头节点,形成闭环。
遍历方向只能从头节点开始,向后遍历到尾节点。可以从任意节点开始,遍历整个链表(闭环结构)。
头指针 vs 尾指针通常只设置头指针,从头节点到尾节点需要遍历整个链表。通常设置尾指针,通过尾指针可以直接访问头节点和尾节点,操作效率更高。
时间复杂度- 访问头节点:O(1)
- 访问尾节点:O(n)
- 插入/删除节点:O(1)
- 插入/删除节点:O(n)
- 访问头节点:O(1)
- 访问尾节点:O(1)
- 插入/删除头节点:O(1)
- 插入/删除尾节点:O(1)
优点- 结构简单,易于实现。
- 适合只需要单向遍历的场景。
- 可以从任意节点遍历整个链表。
- 对表头和表尾的操作效率高(O(1))。
缺点- 无法从尾节点快速访问头节点。
- 对表尾的操作效率低(O(n))。
- 实现稍微复杂,需要处理闭环逻辑。
- 如果不小心处理,可能导致死循环。
适用场景- 只需要单向遍历的场景。
- 对表头操作频繁的场景。
- 需要频繁操作表头和表尾的场景。
- 需要从任意节点遍历整个链表的场景。

循环双链表 

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

循环双链表实现约瑟夫环

有头

#include <iostream>
using namespace std;
int n, k;
typedef struct node {
	struct node * next,*pr;
	int data;
}Node,*Link;


bool initialize(Link &a) {
	a = (Node *)malloc(sizeof(Node));
	if (a == NULL) return false;
	a->next = a;
	a->pr = a;
	return 1;
}

bool Empty(Link &L) {
	if (L->next == L) return 1;
	else return false;
}


//尾插
Link Insert(Link &L) {
	int x;
	//L = (Node *)malloc(sizeof(Node));//在同一个完整代码多余
	Node *p = L;
	
	for(int i=1;i<=n;i++){
		Node * s = (Node*)malloc(sizeof(Node));

		s->next =L;//最后一个指向头节点
		p->next = s;
		s->pr = p;
		L->pr = s;//头节点指向最后一个元素

		s->data = i;
		//移动p
		p = s;
	}
	return L;
}


//删除
void del(Link &L) {

	Node* current = L->next;//第一个
	int remaining = n;
	while (remaining > 1) {
		// 找到第k个节点(移动k-1次)
		for (int i = 1; i < k; ++i) {
			current = current->next;
			if (current == L) { // 如果遇到头节点,跳过
				current = current->next;
			}
		}

		//删除
		Node *temp = current;
		current->next->pr = current->pr;
		current->pr->next = current->next;

		current = current->next;// 将 current 移动到下一个节点
		if (current == L) { // 如果遇到头节点,跳过再下一个
			current = current->next;
		}
		free(temp);//交换顺序出现错误

		remaining--;
	}
	// 输出最后剩下的节点
	cout << current->data << endl;
	L = current;
}


int main()
{	
	cin >> n >> k;
	Link L;
	initialize(L);
	Insert(L);
	del(L);
	return 0;
}

无头

#include <iostream>
using namespace std;

int n, k;

typedef struct node {
    struct node *next, *pr;
    int data;
} Node, *Link;

bool initialize(Link &a) {
    a = NULL; // 初始化为空链表
    return true;
}

Link Insert(Link &L) {
    if (n < 1) return L;

    // 创建第一个节点
    L = new Node;
    L->data = 1;
    L->next = L;
    L->pr = L;

    Node *p = L;

    for (int i = 2; i <= n; i++) {
        Node *s = new Node;
        s->data = i;

        // 将新节点插入到链表尾部
        s->next = L;     // 新节点的next指向头节点
        s->pr = p;       // 新节点的前驱指向当前尾节点p
        p->next = s;     // 当前尾节点的next指向新节点
        L->pr = s;       // 头节点的前驱指向新尾节点
        p = s;           // 更新尾节点指针
    }

    return L;
}

void josephus(Link &L) {
    if (!L || n == 0) return;

    Node *current = L;
    int remaining = n;

    while (remaining > 1) {
        // 找到第k个节点(移动k-1次)
        for (int i = 1; i < k; ++i) {
            current = current->next;
        }

        // 移除当前节点
        Node *prev = current->pr;
        Node *nextNode = current->next;

        // 调整前后节点的指针
        prev->next = nextNode;
        nextNode->pr = prev;

        // 释放当前节点并移动到下一个节点
        Node *temp = current;
        current = nextNode;
        delete temp;

        remaining--;
    }

    // 输出最后剩下的节点
    cout << current->data << endl;
    L = current;
}

int main() {
    cin >> n >> k;
    Link L = NULL;
    initialize(L);
    Insert(L);
    josephus(L);
    return 0;
}

静态链表

静态链表:用数组的方式实现的链表
优点:增、删操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变
适用场景:①不支持指针的低级语言;②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)

静态链表用代码表

图二实现的两种方式代码

#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型数组;
 
 
#include <stdio.h>
#define MAX_SIZE 10
#define EMPTY -2
#define END -1

typedef struct {
    int data;
    int next;
} Node;

Node nodes[MAX_SIZE];
int head;

void initializeList() {
    head = 0; // 头节点
    nodes[head].next = END; // 初始时链表为空
    for (int i = 1; i < MAX_SIZE; i++) {
        nodes[i].next = EMPTY; // 标记为空节点
    }
}

//找到空节点的下标
int findEmptyNode() {
    for (int i = 1; i < MAX_SIZE; i++) {
        if (nodes[i].next == EMPTY) {
            return i;
        }
    }
    return -1; // 没有空节点
}

//插入新节点的位置 位置从0开始
void insert(int index, int data) {
    if (index < 1) {
        printf("插入位置必须大于0\n");
        return;
    }

    int emptyNode = findEmptyNode();//找到空节点的下标
    if (emptyNode == -1) {
        printf("链表已满,无法插入\n");
        return;
    }

    int prev = head;//从链表的头部开始遍历

    for (int i = 0; i < index - 1; i++) {//找到插入位置的前一个节点
        prev = nodes[prev].next;
        if (prev == END) {
            printf("插入位置超出链表长度\n");
            return;
        }
    }

    nodes[emptyNode].data = data;
    nodes[emptyNode].next = nodes[prev].next;
    nodes[prev].next = emptyNode;
}

void deleteNode(int index) {
    if (index < 1) {
        printf("删除位置必须大于0\n");
        return;
    }

    int prev = head;
    for (int i = 0; i < index - 1; i++) {
        prev = nodes[prev].next;
        if (prev == END) {
            printf("删除位置超出链表长度\n");
            return;
        }
    }

    int current = nodes[prev].next;
    if (current == END) {
        printf("删除位置超出链表长度\n");
        return;
    }

    nodes[prev].next = nodes[current].next;
    nodes[current].next = EMPTY; // 标记为空节点
}

void printList() {
    int current = nodes[head].next;
    while (current != END) {
        printf("%d -> ", nodes[current].data);
        current = nodes[current].next;
    }
    printf("NULL\n");
}

int main() {
    initializeList();

    insert(1, 10);
    insert(2, 20);
    insert(3, 30);
    printList(); // 输出: 10 -> 20 -> 30 -> NULL

    deleteNode(2);
    printList(); // 输出: 10 -> 30 -> NULL

    return 0;
}

顺序表和链表的比较

可以围绕这个框架答题。比如:逻辑结构、存储结构、基本操作

特性顺序表链表
存储结构顺序存储链式存储
优点支持随机存取,存储密度高离散的小空间分配方便,容量改变方便
缺点需要连续空间,容量不易扩展不支持随机存取,存储密度低,操作不便
创建方式静态分配(静态数组,容量不可变),动态分配(动态数组,容量可变)只需要分配一个头结点或声明一个头指针
销毁方式静态数组系统自动回收;动态数组需要手动释放内存(free)需要手动释放每个节点的内存空间
增/删操作时间复杂度:O(n),需要移动大量元素(后移/前移)时间复杂度:O(n),需要查找目标元素再修改指针
查找操作按位查找:O(1),按值查找:O(n);若有序则为O(log2n)按位查找:O(n),按值查找:O(n)
存储方式特点连续存储,逻辑顺序与物理顺序一致,存储密度大,空间利用率高

非连续存储,通过指针连接各个节点

空间分配固定大小或动态扩展(需要预留空间)动态分配空间,按需分配,每个节点占用不同空间

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

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

表长难以预估、经常要增加/删除元素 一一链表
表长可预估、查询(搜索)操作较多 一一顺序表 

补充

结构体比较

        结构体元素的比较需要手动进行,因为结构体本身不支持直接使用 == 或 != 等运算符进行比较。

  • bool arePointsEqual(struct Point p1, struct Point p2) {
        return p1.x == p2.x && p1.y == p2.y;
    }
  • struct Point {
        int x;
        int y;
    
        bool operator==(const Point& other) const {
            return x == other.x && y == other.y;
        }
    };

DNode L; 和 DLinklist L; 的区别

  • DNode L;

    • 这里 L 是一个 DNode 类型的变量,即一个具体的双链表结点

    • L 是一个结构体变量,它在栈上分配内存,存储的是一个结点的数据(dataprior 和 next)。

    • L 不是一个指针,因此不能通过 L 来动态管理链表的内存(例如分配或释放结点)。

  • DLinklist L;

    • 这里 L 是一个 DLinklist 类型的变量,即一个指向 DNode 的指针

    • L 是一个指针变量,它可以指向动态分配的内存(例如通过 malloc 分配的头结点)。

    • L 用于表示链表的头指针,通过它可以访问整个链表。

因此这里只能是

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

为什么要L等于malloc,我们这个L不是初始化就分配空间了吗?

        在函数 List_TailInsert 中,L 是一个指向单链表头结点的指针(即 LinkList 类型)。在调用该函数时,L 可能是一个未初始化的指针,指向一个随机的内存地址,而不是一个有效的链表头结点。因此,L = (LinkList)malloc(sizeof(LNode)); 的作用是显式地为头结点分配内存,确保 L 指向一个合法的头结点。这是初始化链表的关键步骤。

  • L = (LinkList)malloc(sizeof(LNode)); 的作用

    • 为头结点分配内存,初始化链表。

    • 确保 L 是一个合法的指针,指向一个有效的头结点。

  • 为什么需要 malloc

    • 链表是动态数据结构,需要在运行时动态分配内存。

    • 头结点是链表的起点,必须显式分配内存。

  • 如果没有 malloc

    • L 是未初始化的指针,访问它会导致程序崩溃。

双链表后插

必须是这个顺序,书中的后插。题中的前插不符合

参考推荐:

数据结构 | 顺序表、链表和数组是逻辑结构还是物理(存储)结构?_顺序表是逻辑结构还是存储结构-优快云博客

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值