链表就是由很多个结点构成的线性结构,每个结点里面存储着本身需要存储的数据和下一个数据的存放地址,可以理解为套娃,A可以找到B,B可以找到C,C可以找到D...
下图为线性链表,还有循环链表和双向链表,后续我会讲解。
一、线性链表的数据对象LNode和*LinkList
线性链表的数据对象特别简单,就是存储的数据和存储下一个结点的地址,因此数据对象如下:
为什么要创建指针结点?
因为生成节点需要为结点用malloc动态分配内存,需要指针去接收分配内存的首地址
typedef int ElemType;
typedef struct LNode {
ElemType data; //存储数据
struct LNode* next; //存储下一个结点的地址
}LNode, *LinkList; //指针用于分配内存
二、动态生成线性表链表CreateList_L
分析:
1. 为头结点L申请内存,头结点中没有数据,且*next为NULL
2. 创建一个新的结点P,并为结点P赋值上数据
3. 让新结点P的*next指向NULL,头结点L的*next指向P。
但是别急,我们多分析两个节点,如果现在我们用上述方法接着往后插入节点,我们就需要让上一个结点的*next指向新的结点,我们要找到前一个结点。这个时候我们就要用一个变量记录一共有了多少个结点,然后不断循环让指针指到上一个结点,太麻烦了。看接下来的操作即可。
4. 我们创建第二个结点P
5. 这一步就像A->B,变成A->C->B,原来A指向的B,现在把A的指向给C就能实现C->B,再让A的指向指向C,就能实现A->C->B。
原来L的*next指向data1,现在把L的指向给data2,就实现了data2的*next指向data1,再让L的*next指向data2,就能实现L->data2->data1。而这个过程中data1的*next始终为NULL,插入方法就是将结点插入到头结点之后,而第一个进去的结点始终是最后一个结点。
6. 我们接着来创建第三个结点
7. 调整指向
8. 完成
总结:
//动态创建链表
void CreateList_L(LinkList& L, int n) {
//创建头结点
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL; //头结点的next为NULL
for (int i = 0; i < n; i++) { //遍历存入输入
LinkList P = (LinkList)malloc(sizeof(LNode)); //创建新节点P
scanf("%d", & P->data); //输入P的数据
P->next = L->next; //P的next为L的next
L->next = P; //头结点L的next指向新结点
}
}
三、访问元素
现在我们想要拿到位置为3的元素,首先我们要定义一个指针指向第一个元素,也就是L的*next。然后循环遍历i-1次也就是P的指向从1到3,就可以让P指向下标为3的位置。最后读取P的数据就行。
总结:
//查找元素
bool GetElem_L(LinkList L, int i, ElemType &e) {
//定义一个指针指向头结点
LinkList P = L;
int j = 1;
while (P && j < i) { //如果P不为空且查找P指向下标不为n
P = P->next; //P指向下一个结点
j++;
}
if (!P || j > i) return false; //如果上面遍历结束P为空(n超出范围)或者查找超出指定范围则返回false
e = P->data; //否则返回P指向i结点的数据
return true;
}
四、插入元素
我们以在位置3插入data4为例子
分析:
定义一个指针P,指向头结点,P的作用是指向插入结点的上一个结点,做链接工作。
1. 循环i-2次,让指针P指向i的前一个结点,也就是要删除的结点
2. 创建一个新的结点S,并为其data赋值上data4
3. 按照新建结点的步骤插入结点
4. 完成
总结:
//插入结点
bool ListInsert_L(LinkList L, int i, ElemType e) {
//定义指针P指向第一个结点
LinkList P = L;
int j = 1;
while (P && j < i-1) { //在P不为空的情况下遍历n-2次
P = P->next; //最终P指向i-1位置的结点
j++;
}
if (!P || j > i) return false; //不符合条件,插入失败
LinkList S = (LinkList)malloc(sizeof(LNode)); //为新节点分配空间
S->data = e; //为新节点赋data值
//将结点S接入到链表中
S->next = P->next;
P->next = S;
return true;
}
五、删除元素ListDelete_L
以删除第三个结点为例
分析:
1. 定义指针P,指向头结点,遍历到i-1个元素,这一步和插入结点的(1)(2)步操作相同
2. 定义指针q为P的*next,那么q的*next就指向的是data4
3. 让P的*next指向data4结点,也就是把q的*next赋值给P的*next。
4. 最后free掉q就完事儿了
总结:
//删除结点
void ListDelete_L(LinkList& L, int i, ElemType &e) {
//获取第i-1个结点
LinkList P = L;
int j = 1;
while (P && j < i) {
P = P->next;
j++;
}
LinkList q = P->next; //q为删除的结点
e = q->data; //把删除的数据拿走
P->next = q->next; //结点i-1和结点i+1连接起来
free(q); //释放删除节点
}
六、归并有序链表MergeList_L
已知两个元素按值非递减排列的单链线性表La = 3,5,8,11,Lb = 2,6,8,9,11,15,20
现将La和Lb归并得到新的单链线性表Lc,Lc的元素也按值非递减排列
分析:
1. 定义pa指向La的第一个结点,pb指向Lb的第一个节点。把La的头结点作为Lc的头结点,pc指向Lc的头结点(也就是La的头结点)
2. 比较pa结点和pb结点的大小,让pc的*next指向小的结点,也就是把小的结点接入到Lc中。
3. 将pc移动到pb(元素小的结点)的位置,pb向后移动一位
4. 接着比较pa结点和pb结点的大小,让pc的*next指向小的结点
5. 将pc移动到pa(元素小的结点)的位置,pa向后移动一位
6. 接着4,5步的内容
7. 当一直执行如上操作直到La和Lb有一个结束时(即pa或者pb指向NULL),结束判断循环
8. 将剩下的部分接入Lc中,并释放Lb
9. 我们整理一下上图,红色箭头就是Lc中*next的指向
总结:
//归并两个链表
void MergeList_L(LinkList La, LinkList Lb, LinkList& Lc) {
LinkList pa = La->next;
LinkList pb = Lb->next;
LinkList pc = Lc = La;
while (pa && pb) { //pa或者pb不为NULL
//判断pa结点和pb结点的大小
if (pa->data > pb->data) { //如果pb结点比pa结点小
pc->next = pb; //将小的结点接入Lc
pc = pb; //pc指向小的结点
pb = pb->next; //pb向后移
}
else { //如果pa结点比pb结点小
pc->next = pa;
pc = pa;
pa = pa->next;
}
}
pc->next = pa ? pa : pb; //将剩下的结点接入Lc中
free(Lb); //释放Lb的头结点
}