链表
线性表链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。
为了表示每个数据元素 ai 与其直接后继数据元素 ai+1 之间的逻辑关系,对数据元素a_i来说,除了其本身的信息之外,还需要存储一个指示其直接后继的信息(直接后继的存储位置)。这两部分信息组成数据元素 ai 的存储映像,称为节点(node)。
结点包括两个域:其中存储数据元素信息的称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。
单链表
只能从前往后走,无法反向遍历。
1.链表存储结构
typeddef int ElemType;
typedef struct node{
ElemType data; //数据域
struct node *next; //指针域(结构体本身类型的指针)
}Node;
2.单链表 - 初始化
Node* initList()
{
Node* head = (Node*)malloc(sizeof(Node));//为头节点分配内存
head->data = 0; //初始化头节点的数据域
head->next = NULL; //初始化头节点的指针域
return head;
}
int main()
{
Node* list = initList();
return 1;
}
3.单链表 - 插入数据
(1)头插法
- 在头节点后面插入数据
int insertHead(Node* L, ElemType e)//ElemType是前面给int起的别名
{
Node* p = (Node*)malloc(sizeof(Node));//创建一个新的节点,存储要插入的数据
p->data = e;//往新创建的节点的数据域中放入e
p->next = L->next;//把传入的L节点指向的下一个节点(指针域)赋值给新创建的p节点
L->next = p;//将头节点的next指针指向要插入的元素
}
int main()
{
Node* list = initList();
insertHead(list, 10);
insertHead(list, 20);
}
必须先将要插入的节点的next指针指向头节点的下一个节点,然后再将头节点的next指针指向要插入的元素,若将这两步调换顺序,先将头节点的next指针指向要插入的元素后链表就在头节点那断开了,要插入的元素也无法链接到下一个元素。
(2)尾插法
- 先获取尾节点地址
Node* getTail(Node* L)
{
Node* p = L;
while(p->next != NULL)
{
p = p->next;
}
return p;
}
尾插法代码的实现:
Node* insertTail(Node* tail, ElemType e)
{
Node* p = (Node*)malloc(sizeof(Node));
p->data = e;
tail->next = p;
p->next = NULL;
return p;//新创建的p节点就是新的尾节点
}
(3)在指定位置插入
int insertNode(Node* L, int pos, ElemType e)
{
Node *p = L;//先对传入来的节点备份
int i = 0;
while(i < pos - 1)//循环到想要插入位置的前一个位置
{
p = p->next;
i++;
if(p == NULL)
{
return 0;
}
}
Node* q = (Node*)malloc(sizeof(Node));//q是要插入的新节点
q->data = e;
q->next = p->next;
p->next = q;
return 1;
}
4.单链表 - 遍历
void listNode(Node* L)
{
Node* p = L->next;//第一个节点(头节点的下一个节点)
while(p != NULL)
{
printf("%d\n",p->data);
p = p->next;
}
printf("\n");
}
5.单链表 - 删除节点
- 第一步:找到要删除节点的前置节点 p
- 第二步:用指针 q 记录要删除的节点
- 第三步:通过改变 p 的后继节点实现删除
- 第四步:释放删除节点的空间
int deleteNode(Node* L, int pos)
{
Node* p = L;
int i = 0;
while(i < pos - 1)//找到要删除节点的前驱
{
p = p->next;
i++;
if(p == NULL)
{
return 0;
}
}
if(p->next == NULL)
{
printf("要删除的位置错误!\n");
return 0;
}
//核心实现步骤
Node *q = p->next;//p是前驱,q指向要删除的节点
p->next = q->next;//让要删除节点的前驱指向要删除节点的后继
free(q);//释放掉删除节点的空间,因为这个节点是malloc出来的
return 1;
}
6.获取链表长度
int listLength(Node* L)
{
Node* p = L;
int len = 0;//声明一个变量用来累加
while(p != NULL)
{
len++;
p = p->next;
}
return len;
}
7.单链表 - 释放链表
把每一个节点的内存空间都释放掉,头节点保留。
- 第一步:指针 p 指向头节点后的第一个节点
- 第二步:判断指针 p 是否指向空节点
- 第三步:如果 p 不为空,用指针 q 记录指针 p 的后继节点
- 第四步:释放指针 p 指向的节点
- 第五步:指针 p 和指针 q 指向同一个节点,循环上面的操作
void freeList(Node* L)
{
Node* p = L->next;
Node* q;
while(p != NULL)
{
q = p->next;
free(p);
p = q;
}
L->next = NULL;
}
单链表的应用
1.快慢指针
- 查找链表中倒数第k个位置上的元素(仅遍历一遍链表)
定义两个指针:快指针(fast)和慢指针(slow),让他们两个都指向第一个节点(头节点的下一个节点),先让快指针走 k 步,然后再让快慢指针同步向后走,知道快指针指向空值,那么此时慢指针指向的那个节点就是倒数第 k 个节点。代码实现如下:
int findNodeFS(Node* L, int k)
{
Node* fast = L->next;
Node* slow = L->next;
for(int i = 0; i < k; i++)
{
fast = fast->next; //快指针先走k步
}
while(fast != NULL)
{
fast = fast->next;
slow = slow->next;
}
printf("倒数第%d个节点值为:%d\n", k, slow->data);
}
- 删除链表中间节点(偶数个时取后面的节点)
用快慢指针,慢指针指向头节点,快指针指向第一个节点,慢指针走一步,快指针走两步,当快指针的下一个节点为空时,慢指针的下一个节点就是要删除的中间节点。代码实现如下:
int delMiddleNode(Node* head)
{
Node *fast = head->next;
Node *slow = head;
while(fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
}
Node *q = slow->next;//指针q保存要删除的节点地址
slow->next = q->next;//跳过要删除的节点,重新连接链表
free(q);
return 1;
}
2.反转链表
Node* reverseList(Node* head)
{
Node *first = NULL;
Node *second = head->next;//从第一个数据节点开始
Node *third;
while(second != NULL)
{
//核心操作
third = second->next;//保存下一个节点
second->next = first;//反转指针
first = second; //first后移
second = third; //second后移
}
//在原链表尾部创建头节点
Node *hd = initList();//上文链表初始化实现的函数
hd->next = first;
}
单向循环链表
循环链表(Circular Linked List)是另一种形式的链式存储结构。其特点是表中最后一个节点的指针域指向头节点,整个链表形成一个环。
当链表遍历时,判别当前指针p是否指向表尾结点的终止条件不同。在单链表中,判别条件为p!=NULL 或p->next!=NULL,而循环单链表的判别条件为p!=L 或p->next!=L。
判断链表是否有环
用快慢指针,快慢指针都指向头节点,快指针走两次,慢指针走一次,若快慢指针相遇则有环,反之则没有。代码实现如下:
int isCycle(Node *head)
{
Node* fast = head;
Node* slow = head;
while(fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
return 1;
}
}
return 0;
}
链表有环入口在哪
- 方法一:先确定环有多少个节点,让快慢指针在走一次至再次相遇,其中每走一步用count值进行记录(count++),然后将快慢指针再次同时指向头节点,让快指针先走count步,再让快慢指针同时走,相遇位置就是环的入口。代码实现如下:
Node* findBegin(Node *head)
{
Node *fast = head;
Node *slow = head;
//先判断有没有环
while(fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
Node *p = fast;
int count = 1;
while(p->next != slow)
{
count++;
p = p->next;
}
fast = head;
slow = head;
for(int i = 0; i < count; i++)
{
fast = fast->next;
}
while(fast != slow)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
}
}
- 方法二(执行效率高,但较难理解):快慢指针相遇,直接让慢指针回链表头,然后快慢指针同速前进,相遇点为入口。代码实现如下:
Node* findBegin(Node* head) {
Node *slow = head;
Node *fast = head;
// 步骤1:判断是否有环
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) { // 相遇,说明有环
// 步骤2:找环入口
slow = head;
while (slow != fast) {
slow = slow->next;
fast = fast->next;
}
return slow; // 相遇节点即入口
}
}
return NULL; // 无环
}
原理:
-
链表头到环入口的距离为 a ;
-
环入口到快慢指针第一次相遇点的距离为 b ;
-
环的长度为 c 。
快慢指针第一次相遇时,快指针走了 a + b + kc ( k 是绕环圈数),慢指针走了 a + b ;又因快指针速度是慢指针的2倍,故 a + b + kc = 2(a + b) ,化简得 a = kc - b,a(链表头->环入口的距离) = kc(绕 k 圈的长度) - b(相遇点->入口的距离) 。
这意味着:从链表头走 a 步,与从相遇点走 (kc - b) 步(等价于绕环 k 圈后再走 c - b 步),会在环入口相遇。
双向链表
链式存储结构的节点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺指针向后寻查其他节点。若要寻查结点的直接前驱、则必须从表头指针出发。换句话说,在单链表中,查找直接后继的执行时间为O(1),而查找直接前驱的执行时间为O(n)。
为克服单链表这种单向性的缺点,可利用双向链表(Double Linked List)。在双向链表的节点中有两个指针域,一个指向直接后继,另一个指向直接前驱。
typedef int ElemType;
typedef struct node{
ElemType data;
struct node *prev, *next;
}Node;
1.双向链表 - 初始化
Node* initList()
{
Node *head = (Node*)malloc(sizeof(Node));
head->data = 0;
head->next = NULL;
head->prev = NULL;
return head;
}
2.双向链表 - 头插法
先将要插入的元素的前后指针都绑定到链表中,然后再将原链表中的前后指针改连到新节点上。
int insertHead(Node* L, ElemType e)
{
Node *p = (Node*)malloc(sizeof(Node));
p->data = e;
p->prev = L;
p->next = L->next;
if(L->next != NULL)
{
L->next->prev = p;//L->next获得头节点的下一个节点,->prev获得第一个节点的前驱指针
}
L->next = p;
return 1;
}
3.双向链表 - 尾插法
先让新节点的前驱指向尾节点,再让尾节点的后继指向新节点,最后将新节点后继赋值为空。
Node* insertTail(Node *tail, ElemType e)
{
Node *p = (Node*)malloc(sizeof(Node));
p->data = e;
p->prev = tail;
tail->next = p;
p->next = NULL;
return p;
}
4.双向链表 - 在指定位置插入数据
int insertNode(Node* L, int pos, ElemType e)
{
Node *p = L;
int i = 0;
while(i < pos - 1)
{
p = p->next;
i++;
if(p == NULL)
{
return 0;
}
}
Node* q = (Node*)malloc(sizeof(Node));
q->data = e;
q->prev = p;
q->next = p->next;
p->next->prev = q;
p->next = q;
return 1;
}
5.双向链表 - 删除节点
- 第一步:找到要删除节点的前置节点 p
- 第二步:用指针 q 记录要删除的节点
- 第三步:通过改变 p 的后继节点以及要删除节点的下一个节点的前驱实现删除
- 第四步:释放删除节点的空间
int deleteNode(Node* L, int pos)
{
Node* p = L;
int i = 0;
while(i < pos - 1)
{
p = p->next;
i++;
if(p == NULL)
{
return 0;
}
}
if(p->next == NULL)
{
printf("要删除的位置错误!\n");
return 0;
}
Node *q = p->next;
p->next = q->next;
q->next->prev = p;
free(q);
return 1;
}
1959

被折叠的 条评论
为什么被折叠?



