数据结构 2 线性表

本文最后更新于:2023年11月30日 晚上

B站王卓老师数据结构的课程学习记录

其中2.5的小结基本实现了线性表的所有功能(Vscode,c/c++)

2.6的小结基本实现了单链表的所有功能(Vscode,c/c++)

有师傅像我一样c没学好,代码有点看不懂的可以看一下2.4,王卓老师的补充讲解很友好

同时也能参考一下下面这篇文章,来进行数据结构的学习

数据结构个人学习推荐 - 乌漆WhiteMoon - 博客园 (cnblogs.com)

2.1 线性表的定义和特点

定义:由n(n ≥ 0)个数据特性相同的元素构成的有限序列称为线性表。

特点:

  1. 线性表中元素的个数n(n ≥ 0)定义为线性表的长度,n = 0时称为空表。
  2. 将非空的线性表(n > 0)记作(a1,a2,a3,...,an)
  3. 这里的数据元素ai(1 ≤ i ≤ n)只是个抽象的符号,其具体含义在不同情况下可以不同。
  4. 在非空的线性表,有且仅有一个开始结点a1,它没有直接前趋,而仅有一个直接后继a2;有且仅有一个终端结点an,它没有直接后继,而仅有一个直接前趋an-1;其余的内部结点ai,(2 < i < n-1)都有且仅有一个直接前趋ai-1和一个直接后继ai+1

2.2 线性表的类型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
ADT List {
数据对象:D={ai| ai ∈ElemSet, i=1,2,…,n, n≥0}
数据关系:R={ < ai-1 , ai> | ai-1, ai∈D, i=1,2,…,n}
基本操作:
InitList (& L )
//(Initialization) 形参或者定义时&是引用,实参或者使用时&是取地址!跟C语言还是C++没有关系!但是引用这个概念只存在于C++
操作结果:构造一个空的线性表L。
DestroyList ( &L )
初始条件:线性表L已存在。
操作结果:销毁线性表L。
ClearList ( &L )
初始条件:线性表L已存在。
操作结果:将L重置为空表。
ListEmpty ( L )
初始条件:线性表L已存在。
操作结果:若L为空表,则返回TRUE, 否则返回FALSE。
Listlenght ( L )
初始条件:线性表L已存在。
操作结果:返回L中数据元素个数。
GetElem (L, i, &e )
初始条件:线性表L已存在,1 ≤i≤ListLength(L)。
操作结果:用e返回L中第 i 个数据元素的值。
LocateElem (L, e, compare() )
初始条件:线性表L已存在,compare()是数据元素判定函数。
操作结果:返回L中第1个与e满足关系compare()的数据元素的位序, 若这样的数据元素不存在,则返回值为0
PriorElem (L, cur_e, &pre_e )
初始条件:线性表L已存在。
操作结果:若cur_e 是L中的数据元素,且不是第一,则用pre_e返回它的前驱,否则操作失败,pre_e无定义。
NextElem (L, cur_e, &next_e )
初始条件:线性表L已存在。
操作结果:若cur_e 是L中的数据元素,且不是最后一个,则用next_e返回它的后继,否则操作失败,next_e无定义。
ListInsert (&L, i, e )
初始条件:线性表L已存在,1 ≤i≤ListLength(L)+1
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
ListDelete (&L, i, &e )
初始条件:线性表L已存在且非空,1 ≤i≤ListLength(L)。
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
ListTraverse (L, visit () )
初始条件:线性表L已存在。
操作结果:依次对L的每个数据元素调用函数visit( )。一旦visit( )失败,则操作失败。
}ADT List

2.3 线性表的顺序表示

把线性表中的所有元素,按照其逻辑顺序依次存储到计算机中的从指定存储位置开始的一块连续的存储空间中

  • 是一种紧凑结构

  • \(Loc(a_{i+1})=loc(a_i)+sizeof(ElemType)\)

  • 是一种随机存储的结构

  • 通常用数组来描述顺序存储结构

  • 顺序表示意图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #define MAXSIZE 20   //储空间初始分配量
    typedef int ElemType; //ElemType类型根据实际情况而定,这里假设为int
    typedef struct{
    ElemType data[MAXSIZE]; //数组存储数据元素,最大值为MAXSIZE
    int length //线性表当前长度
    }SqList;

    /*
    这里,我们就发现描述顺序存储结构需要三个属性:
    存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置
    线性表的最大存储容量:数组长度MaxSize。
    线性表的当前长度:length。
    */
  • 计算地址只算一次与处理数据的规模无关数量级是O(1)这种运算叫随机存取

2.4 类c语言有关操作

2.4.1 元素类型说明

顺序表类型定义

1
2
3
4
typedef struct{
ElemType data[];
int length
}SqList;//顺序表类型

ElemType是根据实际问题,你需要什么类型的数组就定义成什么,一般是根据问题定义一个结构体或者是 typedef char ElemType

1
2
3
4
5
6
7
8
9
10
11
12
//比如,data[]中是abcde,那就能这样
typedef struct{
char data[];
int length
}SqList;//顺序表类型

//或者:
typedef char ElemType
typedef struct{
ElemType data[];
int length
}SqList;//顺序表类型

如果表中元素类型不单一,可以定义一个复杂的结构类型(结构体的嵌套使用):

1
2
3
4
5
6
7
8
9
10
11
typedef struct{
float p;
int e;
}Polynomial;

typedef struct{
Polynomial *elem; //首地址,elem保存地址,这行代码意思是定义了一个Polynomial型的指针变量
int length;
}SqList;


2.4.2 数组定义

数组静态分配:

1
2
3
4
typedef struct{ 
ElemType data[MaxSize];
int length;
}SqList;//顺序表类型

数组动态分配:

1
2
3
4
5
//数组名其实就是首元素的地址,所以也可以直接定义一个指针,也可以表示一个数组,用来存放数组首地址。数组的大小用相应的函数来动态分配内存
typedef struct{
ElemType *data;//数组首地址
int length;
}SqList;//顺序表类型

这种方式我们不知道内存到底有多大,接下来将用内存分配函数来分配空间:

1
2
3
SqList L; //L就是要操作的顺序表,即SqList
//L就有两个成员L.data,用来存放顺序表的元素,另一个是L.length
L.data = (ElemType*)malloc(sizeof(ElemType)*MaxSize);

2.4.3 C语言的内存动态分配

1
2
SqList L; 
L.data = (ElemType*)malloc(sizeof(ElemType)*MaxSize);
  • malloc(m)函数,开辟m字节长度的地址空间,并返回这段空间的首地址,参数m要求是一个整数
  • sizeof(x)运算,计算变量x的长度,即变量需占的字节数×变量个数
  • L.data是数组的首地址(无空间),ElemType*使新分配的空间首地址的元素类型为数组的元素类型,一个“=”就能将放置数组元素类型的空间分配给L了
  • free(p)函数,释放指针p所指变量的存储空间,即彻底删除一个变量
  • L.data = (ElemType*)malloc(sizeof(ElemType)*MaxSize);
    • 后面这个*意思是乘号
    • 前面的()的意思是强制类型转换,
      • (int)强制转换为整数
      • (int *)强制转换为指向整型的一个指针
      • (ElemType*)意思是强制转换成指向数据元素类型ElemType的一个指针,
    • 后面的ElemType表示字节数,比如char需要1个字节,int需要8个字节,那如果MaxSize为100,为char时就需要开辟100字节长度的地址空间,int时就需要开辟800字节长度的地址空间
    • 前面的ElemType是告诉计算机存成什么类型的,比如后面的ElemTypeintMaxSize为100,那一共800个字节要怎么分配呢,如果为char,就被分为800块,如果是int,就被分为200块,划分成什么类型看我们线性表里面的元素是什么类型
  • 需要加载的头文件:<stdlib.h>

2.4.4 C++内容

C++的动态存储分配

new 类型名T(初值列表)

功能:申请用于存放T类型对象的内存空间,并依初值列表赋以初值

结果值:

  • 成功:T类型的指针,指向新分配的内存
  • 失败:0(NULL)
1
2
int *p1 = new int;  //从内存当中动态的分配一块空间,放一个int型,赋给一个指针变量
int *p1 = new int(10); //给空间赋上初值

delete 指针P

功能:释放指针P所指向的内存。P必须是new操作的返回值

1
delete p1;

C++ 的参数传递

传值方式(参数为整型、实型、字符型等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>
using namespace std;

void swap(float m,float n){
float temp;
temp = m;
m = n;
n = temp;
}

int main(){
float a, b;
cout << "Enter two numbers: ";
cin >> a >> b;
cout << "Before swapping: a = " << a << ", b = " << b << endl;

swap(a, b);

cout << "After swapping: a = " << a << ", b = " << b << endl;
return 0;
}

//Enter two numbers: 5 9
//Before swapping: a = 5, b = 9
//After swapping: a = 5, b = 9
  • 说明:函数修改的是形参的值,释放空间后,形参释放,实参的值不变

传地址方式-----指针变量作参数

  • 参数为指针变量

    1. 形参变化影响实参
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include<iostream>
    using namespace std;

    void swap(float *m,float *n){
    float t;
    t = *m;
    *m = *n;
    *n = t;
    }

    int main(){
    float a,b,*p1,*p2;
    cout << "Enter two numbers: ";
    cin>>a>>b;
    cout << "Before swapping: a = " << a << ", b = " << b << endl;

    p1 = &a;p2 = &b; //p1、p2分别指向变量a、b
    swap(p1,p2);

    cout << "After swapping: a = " << a << ", b = " << b << endl;
    }

    //Enter two numbers: 5 9
    //Before swapping: a = 5, b = 9
    //After swapping: a = 9, b = 5
    1. 形参变化不影响实参
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include<iostream>
    using namespace std;

    void swap(float *m,float *n){
    float *t;
    t = m;
    m = n;
    n = t;//交换的是m和n指向的地址不是地址中的内容
    }

    int main(){
    float a,b,*p1,*p2;
    cout << "Enter two numbers: ";
    cin>>a>>b;
    cout << "Before swapping: a = " << a << ", b = " << b << endl;

    p1 = &a;p2 = &b; //p1、p2分别指向变量a、b
    swap(p1,p2);

    cout << "After swapping: a = " << a << ", b = " << b << endl;
    }
    //swap(p1,p2)后没交换
  • 参数为数组名(传递的是数组的首地址,对形参数组所做的任何改变都将反映到实参数组中)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include<iostream>
    #include<cstring> // 包含字符串处理函数的头文件
    using namespace std;

    void sub(char b[]){
    strcpy(b, "world"); // 使用strcpy函数将字符串拷贝到数组中
    }

    int main(void){
    char a[10] = "hello";
    sub(a);
    cout << a << endl;
    }
    //输出world
    //在C++中,你不能直接对整个数组进行赋值操作。正确的方式是使用字符串库函数(如 strcpy)将字符串拷贝到数组中。
  • 参数为引用变量(引用,即给一个对象提供一个替代的名字)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include<iostream>
    using namespace std;
    int main(){
    int i = 5;
    int &j = i;
    i = 7;//公用同一个空间,但是同时有i、j两个名字,因此改变一个值时,另一个值也会改变
    cout<<"i = "<<i<<"\n"<<"j = "<<j;
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //引用类型作参数,用的是同一块空间
    #include<iostream>
    using namespace std;

    void swap(float& m, float& n){
    float temp;
    temp = m;
    m = n;
    n = temp;
    }

    int main(){
    float a, b;
    cin >> a >> b;
    swap(a, b);
    cout << a << endl << b << endl;

    return 0; // 添加 return 0; 语句
    }
    //能成功交换a,b

    引用类型作形参的说明:

    • 传递引用给函数与传递指针的效果是一样的,形参变化实参也发生变化
    • 引用类型作形参,在内存中并没有产生实参的副本,它直接对实参操作;而一般变量作参数,形参与实参就占用不同的存储单元,所以形参变量的值是实参变量的副本。因此,当参数传递的数据量较大时,用引用比用一般变量传递参数的时间和空间效率都好。
    • 指针参数虽然也能达到与使用引用的效果,但在被调函数中需要重复使用 “*指针变量名” 的形式进行运算,这很容易产生错误且程序的阅读性较差,另一方面,在主调函数的调用点处,必须用变量的地址作为实参
    • 所以后面用引用类型作形参比较多

2.5 线性表的顺序表示和实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
#define MAXSIZE 100 //线性表存储空间的初始分配量
//Status 是函数的类型,其值是函数结果状态代码
typedef int Status;

typedef int ElemType; //ElemType的类型根据实际情况而定,这里假定为int
//顺序表数据结构
typedef struct
{
ElemType *elem;
int length;
}SqList;

2.5.1 线性表L的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
//(参数用引用)
Status InitList(SqList &L){ //构造一个空的顺序表L
L.elem = new ElemType[MAXSIZE]; //为顺序表分配空间,
// L.elem = (ElemType *)malloc(MAXSIZE*sizeof(ElemType)); 也可以

// 而下面这句是不行的:L -> elem = (ElemType *)malloc(MAXSIZE*sizeof(ElemType));
//在 C++ 中,使用 -> 运算符来访问指针类型的结构成员,而不是引用。对于结构体 SqList,你应该使用 . 运算符来访问其成员,而不是 -> 运算符。

if(!L.elem) exit(OVERFLOW);
L.length = 0;
return OK;
}

1
2
3
4
5
6
7
8
9
Status InitList(SqList* L){
//构造一个空的线性表L
L -> elem = (ElemType *)malloc(MAXSIZE*sizeof(ElemType));
if(!L -> elem){
return ERROR;
}
L -> length = 0;
return OK;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//第一种c++实现
#include<stdio.h>
#include<stdlib.h>

#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define OVERFLOW -2
#define MAXSIZE 100 //线性表存储空间的初始分配量

//Status 是函数的类型,其值是函数结果状态代码
typedef int Status;

typedef int ElemType; //ElemType的类型根据实际情况而定,这里假定为int
//顺序表数据结构
typedef struct{
ElemType *elem;
int length;
}SqList;

Status InitList(SqList &L){ //构造一个空的顺序表L
L.elem = new ElemType[MAXSIZE]; //为顺序表分配空间,
if(!L.elem) exit(OVERFLOW);
L.length = 0;
return OK;
}

// Function to print the elements of SqList
void OutPut(SqList L) {
printf("Length: %d, Elements: ", L.length);
for (int i = 0; i < L.length; i++) {
printf("%d ", L.elem[i]);
}
printf("\n");
}

int main(){
SqList L;
printf("------构造一个空的线性表L------\n");
InitList(L); //注意观察这条语句的差别!!!
OutPut(L); //打印结果
}

程序运行截图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//第二种c++实现

/* …………
跟前面一样
*/

Status InitList(SqList* L){
//构造一个空的线性表L
L -> elem = (ElemType *)malloc(MAXSIZE*sizeof(ElemType));
if(!L -> elem){
return ERROR;
}
L -> length = 0;
return OK;
}

void OutPut(SqList L) {
//跟前面一样
}

int main(){
SqList L;
printf("------构造一个空的线性表L------\n");
InitList(&L); //@@@@@@@@@@
OutPut(L); //打印结果

}

程序运行截图

2.5.2 销毁、清空、求长度,判断是否为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//销毁线性表L
void DestoryList(Sqlist &L){
if(L.elem) delete L.elem;
}

//清空线性表L
void ClearList(Sqlist &L){
L.length = 0;
}

//求线性表长度
int GetLength(SqList L){
return L.length;
}

//判断线性表是否为空
int IsEmpty(SqList L){
if(L.length == 0) return 1;
else return 0;
}

2.5.3 获取顺序表某一位置上的元素

1
2
3
4
5
6
7
8
9
10
/* 初始条件:顺序表L已存在
操作结果:用e返回L中第i个数据元素的值 */
Status GetElem(SqList L, int i, ElemType &e){
if(i<1 || i>L.length){
return ERROR;
}
e = L.elem[i-1];
return OK;
}

2.5.4 顺序表的查找

1
2
3
4
5
6
7
int LocateElem(SqList L,ElemType e){
//在线性表中查找值为e的数据元素,返回其序号(是第几个元素)
for(i = 0; i<L.length; i++)
if(L.elem[i] == e) return i+1;
return 0;
}

2.5.5 顺序表的插入

1
2
3
4
5
6
7
8
9
10
Status ListInsert_Sq(SqList &L,int i,ElemType e){
if(i<1||i>L.length+1)return ERROR;// 1 i值不合法
if(L.length == MAXSIZE)return ERROR;// 2 当前存储空间已满
for(j = L.length-1; j >= i-1; j--)
L.elem[j+1] = L.elem[j];// 3 插入位置及以后的元素后移
L.elem[i-1] = e;// 4 将新元素e放入第i个位置
L.length++;// 5 表长增1
return OK;
}

2.5.6 顺序表的删除

1
2
3
4
5
6
7
8
Status ListDelete_Sq(SqList &L,int i){
if((i<1)||(i>L.length)) return ERROR;// 1 i值不合法
for(j=i; j<=L.length-1; j++)
L.elem[j-1]=L.elem[j]; // 2 被删除元素之后的元素前移
L.length--; // 3 表长减1
return 0;
}

⭐线性表小结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
//对上面功能的全部实现,c++代码,但主要还是c语言的东西
#include<stdio.h>
#include<stdlib.h>

#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define OVERFLOW -2
#define MAXSIZE 100 //线性表存储空间的初始分配量

//Status 是函数的类型,其值是函数结果状态代码
typedef int Status;
typedef int ElemType; //ElemType的类型根据实际情况而定,这里假定为int
//顺序表数据结构
typedef struct{
ElemType *elem;
int length;
}SqList;

// Function to print the elements of SqList
void OutPut(SqList L) {
printf("Length: %d, Elements: ", L.length);
for (int i = 0; i < L.length; i++) {
printf("%d ", L.elem[i]);
}
printf("\n");
}

//构造一个空的顺序表L
Status InitList(SqList &L){
L.elem = new ElemType[MAXSIZE]; //为顺序表分配空间,
if(!L.elem) exit(OVERFLOW);
L.length = 0;
return OK;
}


//插入操作
Status ListInsert_Sq(SqList &L,int i,ElemType e){
int j;
if(i<1||i>L.length+1)return ERROR;// 1 i值不合法
if(L.length == MAXSIZE)return ERROR;// 2 当前存储空间已满
for(j = L.length-1; j >= i-1; j--)
L.elem[j+1] = L.elem[j];// 3 插入位置及以后的元素后移
L.elem[i-1] = e;// 4 将新元素e放入第i个位置
L.length++;// 5 表长增1
return OK;
}

//删除操作
Status ListDelete_Sq(SqList &L, int i, int &e){
int j;
if((i<1)||(i>L.length)) return ERROR;// 1 i值不合法
e = L.elem[i];
for(j=i; j<=L.length-1; j++)
L.elem[j-1]=L.elem[j]; // 2 被删除元素之后的元素前移
L.length--; // 3 表长减1
return 0;
}


//获取顺序表某一位置上的元素
Status GetElem(SqList L, int i, ElemType &e){
if(i<1 || i>L.length){
return ERROR;
}
e = L.elem[i-1];
return OK;
}

//顺序表的查找,暂时有点问题
int LocateElem(SqList L,int e){
//在线性表中查找值为e的数据元素,返回其序号(是第几个元素)
for(int i = 0; i < L.length; i++)
if(L.elem[i] == e) return i+1;
return 0;
}


//求线性表长度
int GetLength(SqList L){
return L.length;
}

//判断线性表是否为空
int IsEmpty(SqList L){
if(L.length == 0) return 1;
else return 0;
}




int main(){

int i, j;
SqList L;
printf("------构造一个空的线性表L------\n");
InitList(L);
OutPut(L); //打印结果
printf("------测试插入10个数------\n");
for(int i = 1;i <= 10; i++){
ListInsert_Sq(L,i,i);
}
OutPut(L); //打印结果
printf("------在第三位之前插入0------\n");
ListInsert_Sq(L,3,0);
OutPut(L); //打印结果
printf("------删除第6位的数据------\n");

int e;
ListDelete_Sq(L,6,e);
printf("删除的数据为:%d\n", e);
OutPut(L); //打印结果

printf("------获取元素操作------\n");
GetElem(L,5,e);
printf("得到第5个元素:%d\n", e);

printf("------查找元素操作------\n");
i = LocateElem(L,8);
printf("8在顺序表的序号是:%d\n", i);

printf("------线性表的长度------\n");
i = GetLength(L);
printf("线性表的长度是:%d\n", i);

printf("------线性表是否为空------\n");
i = IsEmpty(L);
printf("线性表是否为空(1空0不空):%d", i);

}

运行截图

优点

  • 存储密度大(结点本身所占用的空间/结点结构所占存储量)

  • 可以随机存取表中任意位置的元素

缺点

  • 插入、删除某一元素需移动大量元素

  • 浪费存储空间

  • 属于静态存储形式,当线性表长度变化较大时,难以确定存储空间的容量,数据元素的个数不能自由扩充

2.6 线性表的链式表示

用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的,甚至是零散的分布在内存的任意位置,链表中元素的逻辑次序与物理次序不一定相同)

那怎么表示数据元素之间的逻辑关系呢?

在存储自己内容的同时也存储下一个元素的地址。

各结点由两个域组成:

  • 数据域:存储元素数值数据
  • 指针域:存储直接后继结点的存储位置,指针域中存储的信息称作指针或链。

2.6.1 链式存储有关的术语

  1. 结点: 数据元素的存储映像。由数据域和指针域两部分组成

  2. n 个结点由指针链组成一个链表,它是线性表的链式存储映像,称为线性表的链式存储结构

  3. 单链表、双向链表、循环链表

    • 结点只有一个指针域的链表称为单链表或线性链表
    • 结点有两个指针域的链表称为双链表
    • 首尾相接的链表叫循环链表
  4. 头结点:为了更加方便对链表进行操作,会在单链表的第1个结点前附设一个头结点。头结点的数据域可以不存储任何信息,也可以存储如线性表的长度等附加信息,头结点的指针域存储指向线性表第1个元素的结点。

  5. 头指针:指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针;头指针具有标识作用,所以常用头指针冠以链表的名字;无论链表是否为空,头指针均不为空。头指针是链表的必要元素

  6. 头结点:头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了头结点不一定是链表必须要素

  7. 首元结点:是指链表中存储第一个数据元素的结点

头指针,头结点,首元结点

讨论1:表示空表

如何表示空表

讨论2:有头结点有什么好处?

  1. 便于首元结点的处理 首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理
  2. 便于空表和非空表的统一处理 无论链表是否为空,头指针都是指向头结点的非空指针因此空表和非空表的处理也就统一了

当链表不设头结点时,假设L为单链表的头指针,它应该指向首元结点,则当单链表为长度n为0的空表时,L指针为空(判定空表的条件可记为:L==NULL)。增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。头指针指向头结点。若为空表,则头结点的指针域为空(判定空表的条件可记为:L ->next== NULL)

讨论3: 头结点的数据域内装的是什么?

头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。

顺序表每个元素的存储位置都可从线性表的起始位置计算得到。

而在单链表中,各个元素的存储位置都是随意的。取得第i个数据元素必须从头指针出发顺链进行寻找,也称为顺序存取的存取结构。

顺序表是随机存取而链表是顺序存储

2.6.2 单链表的定义和表示

带头节点的单链表

带头节点的单链表

单链表是由若干个结点构成,所以先定义一下结点。每一个结点都是有两部分组成,一部分是数据元素本身(数据域data),其数据类型根据实际问题的需要确定。另一部分是指向下一个元素(结点)的指针(指针域next)存放下一个元素的地址,结点可以用C语言中的结构体实现当中包含两个成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*线性表的单链表存储结构*/
typedef struct LNode{ //声明结点的类型和指向结点的指针类型
ElemType data; // 结点的数据域
struct LNode *next; // 结点的指针域,这里用自己来定义自己
}LNode,*LinkList;
// LNode 就表示定义了一个结点,如LNode a,那么可以操作a.data,a.next
// LinkList为指向结构体Lnode的指针类型
// 比如头指针,它是指向这种节点的(有数据域有指针域)一个指针,那可以定义为:LNode *L,或者可以LinkList L

/*

struct LNode{
ElemType data;
struct LNode *next;
}; //这一部分我们是定义了一个结构类型
//加上typedef是将这种结构类型重新起了一个名字

*/

// 定义链表L
LinkList L; //或者LNode *L,通常用LinkList定义单链表,强调定义的是某个单链表的头指针
// 定义结点指针p
LNode *p; //或者LinkList p,通常用LNode *定义指向单链表中任意结点的指针变量


例如,存储学生学号、姓名、成绩的单链表结点类型定义如下

1
2
3
4
5
6
typedef struct student{
char num[8]; //数据域
char name[8]; //数据域
int score; //数据域
struct student *next; //指针域
}Lnode,*LinkList;
存储学生学号、姓名、成绩的单链表结点示意图

为了统一链表的操作,通常这样定义

1
2
3
4
5
6
7
8
9
10
typedef struct{
char num[8]; //数据域
char name[8]; //数据域
int score; //数据域
}ElemType;

typedef struct Lnode{
ElenType data; //数据域
struct Lnode *next; //指针域
}Lnode,*LinkList;

2.7 单链表基本操作的实现(带头节点)

2.7.1 单链表的初始化

[算法步骤]

  1. 生成新结点作为头结点,用头指针L指向头结点。

  2. 头结点的指针域置空

[算法描述]

1
2
3
4
5
6
7
8
//LinkList定义如上

Status InitList(LinkList &L){
L = new LNode; //或者用c语法:L = (LinkList)malloc(sizeof(LNode));(LinkList)是强制类型转换
L -> next = NULL; //头结点的指针域置空,指针域就是这个指针所指的结点的next域
//指针变量怎么操作它所指的结点的next的域呢,指针变量要操作某个成员,用->
return OK;
}

2.7.2 判断是否为空、销毁、清空、求表长

[补充算法1]

判断链表是否为空

空表:链表中没有元素,但头指针和头结点仍存在,只是头结点的指针域为空

[算法思路]

判断头结点的指针域是否为空

1
2
3
4
5
6
7
int isEmpty(LinkList L){  //若L为空表返回1,非空返回0
if(L->next) //非空
return 0;
else
return 1;
}

[补充算法2]

单链表的销毁:链表销毁后不存在

[算法思路]

从头指针开始,依次释放所有结点

1
2
3
4
5
6
7
8
9
10
11
12
Status DestoryList(LinkList &L){  //销毁单链表L
LNode *p; //或者LinkList p
while(L){
p = L; //从头结点开始
L = L -> next; //让一个指针指向下一个结点,L -> next的值就是下一个结点的地址
delete p;
//c++:L = new LNode,delete p
//c:L = (LinkList)malloc(sizeof(LNode)),free(p)

}
return OK;
}

[补充算法3]

清空链表:链表仍然存在,但链表中无元素,成为空链表(头结点和头指针仍然存在)

[算法思路]

依次释放所有结点,并将头结点指针域设置为空

1
2
3
4
5
6
7
8
9
10
11
12
Status ClearList(LinkList &L){  //将L重置为空表
LNode *p,*q; //或者LinkList p,q;
p = L ->next; //p指向第一个结点
while(p){
q = p -> next;
delete p;
p = q;

}
L -> next = NULL;
return OK;
}

[补充算法4]

求单链表的表长

[算法思路]

从 首元结点开始,依次计数所有结点

1
2
3
4
5
6
7
8
9
10
11
int ListLength(LinkList L){   //返回L中数据元素的个数
//会对表的内容改变用&L,不改变就用L
Lnode *p; //或者LinkList p
p = L -> next; //p指向第一个结点
int i = 0;
while(p){
i ++;
p = p -> next;
}
return i;
}

总结:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//类型定义
typedef struct LNode{ //声明结点的类型和指向结点的指针类型
ElemType data; // 结点的数据域
struct LNode *next; // 结点的指针域,这里用自己来定义自己
}LNode,*LinkList;

//变量定义
LinkList L;
LNode *p,*s;

//重要操作
p = L; //p指向头结点
s = L->next; //s指向首元结点
p = p->next; //p指向下一结点

2.7.3 获取单链表某一位置上的元素

【算法步骤】

  1. 从第1个结点 (L->next) 顺链扫描,用指针p指向当前扫描到的结点,p初值p = L->next。
  2. j 做计数器,累计当前扫描过的结点数,j 初值为1
  3. 当 p 指向扫描到的下一结点时,计数器 j 加1
  4. 当 j==i 时,p所指的结点就是要找的第 i 个结点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//初始条件:L已存在
//操作结果:用e返回L中第i个数据元素的值
Status GetElem(LinkList L,int i,ElemType &e){
int j;
LNode *p;
p = L->next; //让p指向链表L的第一个结点
j = 1; //j为计数器
while(p && j<i){ //向后扫描,直到p指向第i个元素或p为空
p = p->next;
++j;
}
if(!p || j>i) return ERROR; //第i个元素不存在
e = p -> data; //第i个元素的数据
return OK;

}

2.7.4 单链表的按值查找

根据直到数据获取该数据所在的位置(地址)

【算法步骤】

  1. 从第1个节点依次与e比较
  2. 如果找到一个与e值相等的数据,则返回在列表中的地址或位置
  3. 如果查遍整个链表都没有找到和e相等的元素,返回0/NULL
1
2
3
4
5
6
7
8
9
Lnode *LocateELem_L (LinkList L, Elemtype e){
//在线性表L中查找值为e的数据元素
//找到,则返回L中值为e的数据元素的地址,查找失败返回NULL
p=L->next;
while(p && p->data!=e)
p=p->next;
return p;
}
//代码不是很完整
1
2
3
4
5
6
7
8
9
10
11
//在线性表L中查找值为e的数据元素的位置序号
int LocateElem(LinkList L, Elemtype e){
LinkList p = L->next;
int j = 1;
while (p && p->data != e) {
p = p->next;
j++;
}
if (p) return j;
else return 0;
}

2.7.5 单链表的插入

单链表的插入步骤

互换后:我指向我自己,ai的地址会丢失

想互换,可以多加一个指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//初试条件:L存在
//操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
Status ListInsert(LinkList &L,int i,ElemType e){
int j = 0;
LNode *p,*s;
p=L;
while (p && j< i-1){ //寻找第i-1个结点
p = p -> next;
j++;
}
if(!p || j > i-1) return ERROR;
s = new LNode;
s -> data = e;
s->next = p->next;
p->next=s;
return OK;

}

2.7.6 单链表的删除

删除第i个结点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//初始条件:L存在
//操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
Status ListDelete(LinkList &L, int i, ElemType &e){
int j;
LNode *p,*q;
p = L;
j = 0;
whlie(p->next && j<i-1){ //遍历寻找第i-1个元素
p = p->next;
++j;
}
if(!(p->next) || j>i-1) return ERROR; //第i个元素不存在
q = p->next; //临时保存被删结点的地址以备释放
p->next = q->next; //q的后继赋值给p的后继
e = q->data; //将q结点中的数据给e
delete q;
return OK;
}

2.7.7 单链表的建立

头插法(前插法)

——元素插入在链表头部

头插法
头插法建立单链表
1
2
3
4
5
6
7
8
9
10
11
12
//头插法创建单链表
void CreateList_L(LinkList &L, int n) {
L = new LNode;
L -> next = NULL; //先建立一个带头结点的单链表
for(i = n; i>0; --i){
p = new LNode;//生成新结点,c语言是:p = (LNode*)malloc(sizeof(LNode))
scanf(&p->data);// c++是 cin>>p->data;
p->next = L->next; //插入到表头
L->next = p;
}
}

尾插法(后插法)

尾插法建立单链表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//正位序输入n个元素的值,尾插法创建单链表
void CreateList_R(LinkList &L, int n) {
L = new LNode;
L -> next = NULL; //先建立一个带头结点的单链表
LNode *r;
r = L; //尾指针r指向头结点
for(i = n; i>0; --i){
p = new LNode;//生成新结点,c语言是:p = (LNode*)malloc(sizeof(LNode))
scanf(&p->data);// c++是 cin>>p->data;
p -> next = NULL;
r -> next = p; //插入到表尾
r = p; //r指向新的尾结点
}
}

⭐单链表小结

下面是在Vscode上写的代码,实现了上面所述的单链表的所有功能

注意销毁单链表的操作,笨🐭这里把它注释掉了,不然还要改,想测试的话可以ctrl + /

还要注意在讨论链表的状态时,"链表为空" 和 "链表不存在" 是两种不同的情况:

  1. 链表为空(Linked List is Empty): 这指的是链表被创建了,但没有添加任何元素,或者之前的元素都被删除了。在这种情况下,链表的头结点存在,但头结点的 next 指针为空,没有其他有效的结点。
  2. 链表不存在(Linked List does not exist): 这指的是链表根本没有被创建。在这种情况下,头结点都没有被分配内存,链表没有被初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
#include<stdio.h>
#include<stdlib.h>

#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0


typedef int Status;
typedef int ElemType; //ElemType的类型根据实际情况而定,这里假定为int


/*线性表的单链表存储结构*/
typedef struct LNode{ //声明结点的类型和指向结点的指针类型
ElemType data; // 结点的数据域
struct LNode *next; // 结点的指针域,这里用自己来定义自己
}LNode,*LinkList;



// 输出链表中各个结点的元素,区分链表为空和链表不存在
void OutPut(LNode* p) {
if (p == NULL) {
printf("链表不存在(已被销毁\n");
return;
}

if (p->next == NULL) {
printf("链表为空\n");
return;
}

p = p->next;
while (p) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}


//初始化
Status InitList(LinkList &L){
L = new LNode; //或者用c语法:L = (LinkList)malloc(sizeof(LNode));(LinkList)是强制类型转换
L -> next = NULL; //头结点的指针域置空,指针域就是这个指针所指的结点的next域
//指针变量怎么操作它所指的结点的next的域呢,指针变量要操作某个成员,用->
return OK;
}


//头插法创建单链表
void CreateList_L(LinkList &L, int n) {
L = new LNode;
L -> next = NULL; //先建立一个带头结点的单链表
LNode *p;
for(int i = n; i>0; --i){
p = new LNode; //生成新结点,c语言是:p = (LNode*)malloc(sizeof(LNode))
scanf("%d",&p->data); // c++是 cin>>p->data;
p->next = L->next; //插入到表头
L->next = p;
}
}

//尾插法创建单链表
void CreateList_R(LinkList &L, int n) {
L = new LNode;
L -> next = NULL; //先建立一个带头结点的单链表
LNode *r,*p;
r = L; //尾指针r指向头结点
for(int i = n; i>0; --i){
p = new LNode;//生成新结点,c语言是:p = (LNode*)malloc(sizeof(LNode))
scanf("%d",&p->data);// c++是 cin>>p->data;
p -> next = NULL;
r -> next = p; //插入到表尾
r = p; //r指向新的尾结点
}
}

// 尾插法,在已经插入的基础上进行
void CreateList_R2(LinkList &L, int n) {
LNode *r = L; // 尾指针r指向头结点

// 找到链表的尾节点
while (r->next) {
r = r->next;
}

LNode *p;
for (int i = 0; i < n; ++i) {
p = new LNode; // 生成新结点
scanf("%d", &p->data); // 读取数据
p->next = NULL;
r->next = p; // 插入到尾部
r = p; // r指向新的尾结点
}
}

//判断是否为空
int isEmpty(LinkList L){ //若L为空表返回1,非空返回0
if(L->next) //非空
return 0;
else
return 1;
}


//销毁单链表
Status DestoryList(LinkList &L){ //销毁单链表L
LNode *p; //或者LinkList p
while(L){
p = L; //从头结点开始
L = L -> next; //让一个指针指向下一个结点,L -> next的值就是下一个结点的地址
delete p;
//c++:L = new LNode,delete p
//c:L = (LinkList)malloc(sizeof(LNode)),free(p)

}
return OK;
}

//清空单链表
Status ClearList(LinkList &L){ //将L重置为空表
LNode *p,*q; //或者LinkList p,q;
p = L ->next; //p指向第一个结点
while(p){
q = p -> next;
delete p;
p = q;

}
L -> next = NULL;
return OK;
}

//求链表的表长
int ListLength(LinkList L){ //返回L中数据元素的个数
//会对表的内容改变用&L,不改变就用L
LNode *p; //或者LinkList p
p = L -> next; //p指向第一个结点
int i = 0;
while(p){
i ++;
p = p -> next;
}
return i;
}



//获取单链表某一位置上的元素
Status GetElem(LinkList L,int i,ElemType &e){
int j;
LNode *p;
p = L->next; //让p指向链表L的第一个结点
j = 1; //j为计数器
while(p && j<i){ //向后扫描,直到p指向第i个元素或p为空
p = p->next;
++j;
}
if(!p || j>i) return ERROR; //第i个元素不存在
e = p -> data; //第i个元素的数据
return OK;

}

//单链表的按值查找
int LocateElem(LinkList L, int e){
LinkList p = L->next;
int j = 1;
while (p && p->data != e) {
p = p->next;
j++;
}
if (p) return j;
else return -1;
}


//单链表的插入
Status ListInsert(LinkList &L,int i,ElemType e){
int j = 0;
LNode *p,*s;
p=L;
while (p && j< i-1){ //寻找第i-1个结点
p = p -> next;
j++;
}
if(!p || j > i-1) return ERROR;
s = new LNode;
s -> data = e;
s->next = p->next;
p->next=s;
return OK;

}


//单链表的删除
Status ListDelete(LinkList &L, int i, ElemType &e){
int j;
LNode *p,*q;
p = L;
j = 0;
while(p->next && j<i-1){ //遍历寻找第i-1个元素
p = p->next;
++j;
}
if(!(p->next) || j>i-1) return ERROR; //第i个元素不存在
q = p->next; //临时保存被删结点的地址以备释放
p->next = q->next; //q的后继赋值给p的后继
e = q->data; //将q结点中的数据给e
delete q;
return OK;
}


int main(){
int empty;
int length;
int m; //存储从单链表中获取的某一位置上的元素
int n; //存储某一元素在单链表中的位置
int a; //删除操作中删除的那个元素的值

LinkList L;
//构造单链表
InitList(L);
OutPut(L);
empty = isEmpty(L);
printf("判断是否为空(1空0非空):%d",empty);

printf("\n------头插法5个数------\n");
CreateList_L(L,5);
OutPut(L);
empty = isEmpty(L);
printf("判断是否为空(1空0非空):%d",empty);


printf("\n------清空单链表------\n");
ClearList(L);
OutPut(L);
empty = isEmpty(L);
printf("判断是否为空(1空0非空):%d",empty);


printf("\n------尾插法6个数------\n");
CreateList_R(L,6);
OutPut(L);
length = ListLength(L);
printf("链表的长度为:%d",length);

// printf("------销毁单链表------\n");
// DestoryList(L);
// OutPut(L);


printf("\n----用尾插法,在前面插入的基础上再插入3个数----\n");
CreateList_R2(L,3);
OutPut(L);
length = ListLength(L);
printf("链表的长度为:%d",length);

printf("\n------获取单链表某一位置上的元素------\n");
GetElem(L,3,m);
printf("第三个元素的值为:%d",m);


printf("\n------获取某一元素在单链表中的位置------\n");
n = LocateElem(L,88);
printf("88的位置为:%d",n);


printf("\n------单链表的插入------\n");
ListInsert(L,3,100);
OutPut(L);

printf("\n------单链表的删除------\n");
ListDelete(L,1,a);
OutPut(L);
printf("删除的第一个位置上的元素为:%d",a);
}
//虽然什么都没干,但还是忙死我了😎
单链表的所有操作总结

2.8 循环链表

循环链表是头尾相接的链表(即表中最后一个结点的指针域指向头结点,整个链表形成一个环(circular linked list)

循环链表

优点:从表中任一结点出发均可访问全部结点

image-20231130193234977
image-20231130193247819

如果我们经常要操作首尾结点,用尾指针更方便

image-20231130193518153

带尾指针循环链表的合并(将Tb合并在Ta之后)

image-20231130194030814
1
2
3
4
5
6
7
8
9
10
LinkList Connect(LinkList Ta, LinkList Tb){
LNode *ptr;

p = Ta->next; //p保存 Ta 的头结点
Ta->next = Tb->next->next; //Tb表头链接Ta表尾
delete Tb->next; //释放 Tb 的头结点
Tb->next = p; //修改 Tb 尾结点的后继为 Ta 的头结点

return Tb; //返回合并后的头结点
}

2.9 双向链表

2.9.1 双向链表

单链表的结点--->有指示后继的指针域--->找后继结点方便;

即:查找某结点的后继结点的执行时间为 O(1)

--->无指示前驱的指针域--->找前驱结点难: 从表头出发查找

即:查找某结点的前驱结点的执行时间为 O(n)

为了克服单链表的这一缺点,设计了双向链表(double linked list),

双向链表是在单链表的每个结点中再设计一个指向其前驱结点的指针域。所以在双向链表中的结点有两个指针域,一个指向直接后继,另一个指向直接前驱。

双向链表结点结构
1
2
3
4
5
6
//双向链表的存储结构
typedef struct DuLNode{
ElemType data;
struct DuLNode *prior; //前驱指针域
struct DuLNode *next; //后继指针域
}DuLNode,*DuLinkList;
双向链表的存储结构

2.9.2 双向循环链表

image-20231130195409888

双向链表结构有对称性(设指针p指向某一个结点) p->prior->next = p = p->next->prior(前进一步后退一步相当于原地踏步)

在双向链表中有些操作(ListLength,GetElemment)等因为只涉及一个方向的指针,他们的算法与线性表的相同。

但在插入和删除需要修改两个方向上的指针,两者的算法复杂度均为O(n)

2.9.3 双向链表的插入

其实双向链表的结构是下面这样的:

上面的那种经典教程中的经典结构我个人认为有点有失偏颇,会在一定程度上误导我这种笨学生

双向链表的结构
双向链表的插入

同时这里王卓老师讲的双向链表的插入可以用上面那种结构自己画一下示意图,会更好的理解

1
2
3
4
5
6
7
8
9
10
11
12
13
//或者void ListInsert_DuL(DuLinkList &L,int i,ElemType e){}
Status ListInsert_DuL(DuLinkList &L,int i,ElemType e){
//在带头结点的双向链表中第i个位置之前插入元素 e
if(!(p=GetElem_DuL(L,i))) return ERROR; //在L中确定第 i个元素的位置指针 p,p为 NULL时,第i个元素不存在
s = new DuLNode; //生成新结点*s
s->data=e; //将结点*s 数据域置为 e
s->prior = p->prior; //将结点*s 插人L中,对应上图中①
p->prior->next = s; //对应上图中②
s->next = p; //对应上图中③
p->prior = s; //对应上图中④

return OK;
}

2.9.4 双向链表的删除

假设要删除双向链表的结点 ptr,只需要把 ptr 结点的前驱和后继安排明白即可。

双向链表的删除
1
2
3
4
5
6
7
8
9
void ListDelete_DuL(DuLink &L, int i, ElemType &e) {
// 删除带头结点的双向循环链表 L 的第 i个元素,并用 e 返回
if(!(p=GetElem_DuL(L,i))) return ERROR;
e = p -> data;
p -> prior -> next = p -> next;
p -> next -> prior = p -> prior;
free(p);
return OK;
}

2.10 顺序表和链表的比较

链式存储结构的优点:

  • 结点空间可以动态申请和释放;
  • 数据元素的逻辑次序靠结点的指针来指示,插入和删除不需要移动元素。

链式存储结构的缺点:

  • 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占的字节数不多时,指针域所占的存储空间的比重显得很大。存储密度是指结点数据本身占用的空间/结点占用的空间总量
  • 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度。(对某个结点操作一般要先找到该结点)
存储密度的概念
存储密度的例子

顺序表和链表的比较

顺序表和链表的比较

*2.11 应用

ptr -- pointer (即指针)的缩写。

参考资料:

数据结构:链表结构和例题详解 - 乌漆WhiteMoon - 博客园 (cnblogs.com)

LinkList L、LinkList& L、和LinkList *L这三者的区别 (zhihu.com)

关于linklist L 和linklist &L的区别 - 知乎 (zhihu.com)

数据结构- 文集 哔哩哔哩专栏 (bilibili.com)

数据结构:线性表(List)【详解】-CSDN博客

链表的基本操作(C语言)详解 (biancheng.net)


数据结构 2 线性表
http://viper2383.github.io/2023/11/13/数据结构 2 线性表/
作者
w1per3
发布于
2023年11月13日
许可协议