上一篇文章讲了链表相关的概念,这篇主要记录的是和链表相关的算法以及一些写好链表算法代码相关的技巧
。node
// 头文件 ----------------------------------------------------------
typedef struct _node {
int value; // 数据域
struct _node *next; // 指针域
} Node;
typedef struct _list {
Node *head; // 头指针
Node *tail; // 尾指针
int length; // 链表的长度
} SingleList;
// 建立链表
SingleList * creatList(void);
// 释放链表
void freeList(SingleList *pList);
// 给链表末尾添加一个结点
void appendNode(SingleList *pList, int value);
// 删除末尾结点
void removeLastNode(SingleList *pList);
// 在指定位置插入一个结点
void insertNodeAtIndex(SingleList *pList, int index, int value);
// 在指定位置删除一个结点
void deleteNodeAtIndex(SingleList *pList, int index);
// 打印链表中全部结点值
void printList(SingleList *pList);
// .m 文件 --------------------------------------------------------------------
SingleList * creatList(void) {
SingleList *pList = (SingleList *)malloc(sizeof(SingleList));
pList->head = NULL;
pList->tail = NULL;
pList->length = 0;
return pList;
}
// 释放链表
void freeList(SingleList *pList) {
if(pList == NULL) {
return;
}
if (pList->head == NULL) {
free(pList);
return;
}
// 用q 来保存下一个p节点
Node *p, *q = NULL;
for (p = pList->head; p; p = q) {
q = p->next;
free(p);
}
free(pList);
}
// 给链表末尾添加一个结点
void appendNode(SingleList *pList, int value) {
// 制造一个结点,加入链表中去
Node *p = (Node *)malloc(sizeof(Node));
p->value = value;
p->next = NULL;
// 若是链表为空
if (pList->head == NULL) {
// p 结点就是头结点,也是尾结点
pList->head = pList->tail = p;
} else {
pList->tail->next = p;
// 更新尾指针
pList->tail = p;
}
pList->length += 1;
}
// 删除末尾结点
void removeLastNode(SingleList *pList) {
if (pList->tail == NULL) {
// 链表为空
printf("链表为空!!!!");
return;
}
if (pList->head == pList->tail) {
// 链表只有一个结点
pList->head = pList->tail = NULL;
pList->length -= 1;
return;
}
// 须要先遍历的到尾结点的上一个结点,而后删除尾结点,再更新尾结点
Node *p = pList->head;
while (p->next != pList->tail) {
p = p->next;
}
// 释放尾结点
free(pList->tail);
p->next = NULL;
pList->length -= 1;
// 更新尾结点
pList->tail = p;
}
// 在指定位置插入一个结点,下标从 0 开始
void insertNodeAtIndex(SingleList *pList, int index, int value) {
if (index >= pList->length || index < 0) {
// 下标越界
printf("下标不合法!!!");
return;
}
// 制造一个结点,加入链表中去
Node *s = (Node *)malloc(sizeof(Node));
s->value = value;
s->next = NULL;
Node *p = pList->head;
Node *q = NULL;
for (int i = 0; i < pList->length; i ++) {
// 找到了要插入的节点位置
if (i == index) {
if (i == 0) {
// 插入到头结点
s->next = pList->head;
pList->head = s;
} else {
s->next = p;
q->next = s;
}
pList->length += 1;
break;
}
q = p;
p = p->next;
}
}
// 在指定位置删除一个结点
void deleteNodeAtIndex(SingleList *pList, int index) {
if (index >= pList->length || index < 0) {
// 下标越界
printf("下标不合法!!!");
return;
}
Node *p = pList->head;
Node *q = NULL;
for (int i = 0; i < pList->length; i ++) {
if (index == i) {
if (i == 0) {
// 首节点,将链表的首节点指向
pList->head = p->next;
} else {
q->next = p->next;
}
free(p);
pList->length -= 1;
break;
}
// 用 q 来记录 p 的上一个结点
q = p;
p = p->next;
}
}
// 打印链表中全部结点值
void printList(SingleList *pList) {
Node *p = pList->head;
if (p == NULL) {
printf("链表为空!!!");
}
while (p) {
printf("%d\n", p->value);
p = p->next;
}
}
// 测试代码 ------------------------------------------------------
SingleList *pList = creatList();
// 加入结点
printf("------加入结点\n");
appendNode(pList, 10);
appendNode(pList, 20);
appendNode(pList, 30);
appendNode(pList, 40);
appendNode(pList, 50);
printList(pList);
printf("------删除结点\n");
removeLastNode(pList);
printList(pList);
printf("------插入新结点到头结点位置\n");
insertNodeAtIndex(pList, 0, 100);
printList(pList);
printf("------插入新结点到尾结点位置\n");
insertNodeAtIndex(pList, 4, 200);
printList(pList);
printf("------插入新结点到中间结点位置\n");
insertNodeAtIndex(pList, 1, 300);
printList(pList);
printf("------插入新结点到中间结点位置\n");
insertNodeAtIndex(pList, 3, 500);
printList(pList);
printf("------删除头结点\n");
deleteNodeAtIndex(pList, 0);
printList(pList);
printf("------删除尾结点\n");
deleteNodeAtIndex(pList, 6);
printList(pList);
printf("------删除中间结点\n");
deleteNodeAtIndex(pList, 3);
printList(pList);
printf("------删除中间结点\n");
deleteNodeAtIndex(pList, 2);
printList(pList);
// 释放链表
freeList(pList);
// 打印日志 ---------------------------------------------------
------加入结点
10
20
30
40
50
------删除结点
10
20
30
40
------插入新结点到头结点位置
100
10
20
30
40
------插入新结点到尾结点位置
100
10
20
30
200
40
------插入新结点到中间结点位置
100
300
10
20
30
200
40
------插入新结点到中间结点位置
100
300
10
500
20
30
200
40
------删除头结点
300
10
500
20
30
200
40
------删除尾结点
300
10
500
20
30
200
------删除中间结点
300
10
500
30
200
------删除中间结点
300
10
30
200
Program ended with exit code: 0
复制代码
循环链表就是尾结点的next指针指向的是头结点,这样链表就构成了环,实现起来和单链表差很少。主要是循环的条件变成了 p->next != head
,这里用到了哨兵结点,关键代码以下:算法
// 建立链表,至少有一个结点
CycleList * creatList(void) {
CycleList *pList = (CycleList *)malloc(sizeof(CycleList));
// 哨兵结点
Node *head = (Node *)malloc(sizeof(Node));
// 本身的 next 指针指向本身
head->next = head;
pList->head = head;
return pList;
}
// 在指定位置插入一个结点
void insertNodeAtIndex(CycleList *pList, int index, int value) {
int length = listLength(pList);
if (index < 0 || index > length) {
printf("下标不合法!!!!");
return;
}
// 在末尾插入
if (index == length) {
appendNode(pList, value);
return;
}
// 在中间插入
Node *p = pList->head;
int i = 0;
while (p->next != pList->head) {
if (i == index) {
// 插入结点
Node *s = (Node *)malloc(sizeof(Node));
s->value = value;
s->next = p->next;
p->next = s;
}
p = p->next;
i++;
}
}
// 在指定位置删除一个结点
void deleteNodeAtIndex(CycleList *pList, int index) {
int length = listLength(pList);
if (index < 0 || index >= length) {
printf("下标不合法!!!!");
return;
}
Node *p = pList->head;
int i = 0;
while (p->next != pList->head) {
if (i == index) {
if (index == length - 1) {
// 在末尾删除
Node *q = p->next;
p->next = pList->head;
free(q);
return;
} else {
// 在中间删除
Node *q = p->next;
p->next = p->next->next;
free(q);
}
}
p = p->next;
i++;
}
}
复制代码
双向链表既有前驱指针,又有后继指针,因此能够双向遍历
。关键代码以下:数组
// 指定位置插入结点
void insertNodeAtIndex(ListNode *head, int index, int value) {
int length = listLength(head);
if (index < 0 || index > length) {
printf("下标不合法!!!\n");
return;
}
// 在末尾插入
if (index == length) {
appendNode(head, value);
return;
}
// 在中间插入
ListNode *p = head;
int i = 0;
while (p && p->next) {
if (index == i) {
// 插入结点
ListNode *s = (ListNode *)malloc(sizeof(ListNode));
s->value = value;
// 新结点的前驱结点为上一个结点
s->prev = p;
// 新结点的下一个结点的前驱结点为新结点
p->next->prev = s;
// 新结点的后继结点为p的下一个结点
s->next = p->next;
// p 结点的后继结点为s
p->next = s;
}
p = p->next;
i++;
}
}
// 指定位置删除结点
void deleteNodeAtIndex(ListNode *head, int index) {
int length = listLength(head);
if (index < 0 || index >= length) {
printf("下标不合法!!!\n");
return;
}
ListNode *p = head;
ListNode *q = NULL;
int i = 0;
while (p && p->next) {
if (index == i) {
// 保存要删除的结点
q = p->next;
if (index == length - 1) {
// 删除最后一个结点
// 直接让p的next指针置空
p->next = NULL;
} else {
// 删除中间结点
// p 的下一个结点的下一个结点的前驱结点变成p
p->next->next->prev = p;
// p的下一个结点变成下下个结点
p->next = p->next->next;
}
// 释放要删除的结点
free(q);
}
p = p->next;
i++;
}
}
复制代码
解题思路:bash
这个题和合并两个有序的数组为一个有序数组的思路同样,申请第三个链表,长度链表同时遍历,谁的结点比较小就将谁的结点插入到新链表中,最后短链表遍历完,再将长链表中剩余的结点插入到新的链表中去,时间复杂度只有一层循环遍历是 O(n),空间复杂度额外申请了一个 ListNode 空间,来存储新的链表结点,因此是 O(n)。数据结构
/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){
// 异常判断
if (l1 == NULL) {
return l2;
}
if (l2 == NULL) {
return l1;
}
struct ListNode *firsthead = l1;
struct ListNode *secouondhead = l2;
// 用第三个链表来保存数据
struct ListNode *l3 = malloc(sizeof(struct ListNode));
// 临时变量,指向新申请的结点l3
struct ListNode *thirdhead = l3;
// 同时遍历两个长短链表
while (firsthead && secouondhead) {
// 那个链表的结点值比较小就将该结点插入到新链表中
if (firsthead->val <= secouondhead->val) {
l3->next = firsthead;
firsthead = firsthead->next;
}else {
l3->next = secouondhead;
secouondhead = secouondhead->next;
}
l3 = l3->next;
}
if (firsthead == NULL) {
// 还剩第二个链表,将第二个链表中的全部结点都插入到新链表中
while (secouondhead) {
l3->next = secouondhead;
secouondhead = secouondhead->next;
l3 = l3->next;
}
}
if (secouondhead == NULL) {
// 还剩第一个链表,将第一个链表中的全部结点都插入到新链表中
while (firsthead) {
l3->next = firsthead;
firsthead = firsthead->next;
l3 = l3->next;
}
}
// 该结点的next指针指向的才是真正合并后的第一个结点
return thirdhead->next;
}
复制代码
快慢指针就能够实现,快指针走两步,慢指针走一步,遍历完整个链表慢指针指向的就是中间节点。默认链表没有环。若是链表长度是偶数,中间节点取的是下中位节点。app
// 求中间节点
Node *getMiddleNode(SingleList *pList) {
if (pList->head == NULL) {
printf("链表为空!!!");
}
// 快指针,每次走两步
Node *fast = pList->head;
// 慢指针,每次走一步
Node *slow = pList->head;
while (slow && fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
// 测试代码 ---------------------------------
SingleList *pList = creatList();
// 加入结点
printf("------加入结点\n");
appendNode(pList, 10);
appendNode(pList, 20);
appendNode(pList, 30);
appendNode(pList, 40);
appendNode(pList, 50);
printList(pList);
// 中间节点
Node *mid = getMiddleNode(pList);
printf("mid.value = %d\n", mid->value);
// 打印日志 ---------------------------------
------加入结点
10
20
30
40
50
mid.value = 30
复制代码
题目地址post
struct ListNode* reverseList(struct ListNode* head){
if(head == NULL || head->next == NULL) return head;
struct ListNode* provious = NULL;
struct ListNode* current = head;
while(current) {
// 保存当前结点的下一个结点指针
struct ListNode* temp = current->next;
// 将当前节点的next指针指向上一个结点
current->next = provious;
// 将当前节点赋值给上一个结点
provious = current;
// 指向下一个结点
current = temp;
}
// 最后一个provious就是链表的头指针
return provious;
}
复制代码
题目地址学习
解题思路: 每次走两步遍历链表,每次两两交换都须要修改三个结点指针的指向,须要三个变量来保存。其中 previous 用来保存上一次的 current 指针。测试
/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */
struct ListNode* swapPairs(struct ListNode* head) {
struct ListNode *current = head;
struct ListNode *previous = NULL;
while (current && current->next) {
// 当前指针的下一个结点
struct ListNode *a = current->next;
// 当前指针的下下一个结点
struct ListNode *b = a->next;
// 交换
a->next = current;
current->next = b;
if (previous) {
// 将上次的指针指向交换后的结点
previous->next = a;
} else {
// 从新赋值给头指针
head = a;
}
// 保存上一次的指针
previous = current;
// 每次走两步
current = b;
}
return head;
}
复制代码
题目地址ui
解法一: 使用一个散列表老保存遍历过的结点,每次遍历链表都去散列表中查找,判断当前结点是否已经存在散列表中,若是在散列表中找到,那么就有环,若是直到链表遍历结束也没找到,就没有环。这种的时间复杂度是 O(n),空间复杂度是 O(n)。
解法二: 使用快慢指针,遍历链表,快指针每次走两步,慢指针每次走一步,判断快慢指针是否相遇,若是相遇则链表有环,若是遍历完链表也没有相遇,说明没有环。这种解法的时间复杂度是 O(n),空间复杂度是 O(1)。
// 解法二
/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */
bool hasCycle(struct ListNode *head) {
if(head == NULL || head->next == NULL) return false;
// 快慢指针
struct ListNode *fast = head;
struct ListNode *slow = head;
while(slow && fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
// 若是快慢指针相遇,就表示有环
if(slow == fast) {
return true;
}
}
return false;
}
复制代码
解题思路:
给定一个链表,返回链表开始入环的第一个节点。这里运用到了一个几何上的数学公式,快指针和慢指针走一移动直到第一次相遇在 X 结点,假设慢指针走了 N 步,快指针就走了 2N 步,假设入环的第一个节点为 Z,则会有起点到 Z 的距离会等于 X 结点到 Z 的距离,因此在快慢指针判断链表有环后只须要让快指针从头开始一步一步走,以此同时慢指针继续向前走,二者就会在第一个入环节点为 Z 相遇。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head) {
if (head == NULL || head->next == NULL) {
return NULL;
}
struct ListNode *fast = head;
struct ListNode *slow = head;
// 快慢指针检测是否有环
while(slow && fast && fast->next){
fast = fast->next->next;
slow = slow->next;
if(fast == slow) {
break;
}
}
// 有环
if(fast == slow) {
// 寻找环的入环结点
fast = head;
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
return fast;
} else {
return NULL;
}
}
复制代码
解题思路:
每 k 个结点一组翻转,可使用递归实现,每一组翻转完成后,传入下一个结点的指针,递归调用,递归的基线条件是剩余的结点个数小于k,而后须要将每一组翻转翻转的组尾的 next 指针指向下一组的组头。
/* * @lc app=leetcode.cn id=25 lang=c * * [25] k个一组翻转链表 * * https://leetcode-cn.com/problems/reverse-nodes-in-k-group/description/ * * algorithms * Hard (48.65%) * Total Accepted: 10.3K * Total Submissions: 20.3K * Testcase Example: '[1,2,3,4,5]\n2' * * 给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链表。 * * k 是一个正整数,它的值小于或等于链表的长度。若是节点总数不是 k 的整数倍,那么将最后剩余节点保持原有顺序。 * * 示例 : * * 给定这个链表:1->2->3->4->5 * * 当 k = 2 时,应当返回: 2->1->4->3->5 * * 当 k = 3 时,应当返回: 3->2->1->4->5 * * 说明 : * * * 你的算法只能使用常数的额外空间。 * 你不能只是单纯的改变节点内部的值,而是须要实际的进行节点交换。 * * */
/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */
struct ListNode* reverseKGroup(struct ListNode* head, int k){
// 异常判断
if (head == NULL || head->next == NULL || k < 2) {
return head;
}
// 判断链表中的结点是否大于等于k个,还够不够翻转
struct ListNode *temp = head;
for (int i = 0; i < k; i++, temp = temp->next) {
if (temp == NULL) {
// 不够翻转,基线条件
return head;
}
}
// k个结点一组进行结点的翻转
struct ListNode *current = head;
struct ListNode *previous = NULL;
for (int i = 0; i < k; i ++) {
struct ListNode *temp = current->next;
current->next = previous;
previous = current;
current = temp;
}
// 递归条件,正好每组的头结点翻转完成后到组尾,将next指针指向下一组的组头结点
head->next = reverseKGroup(current, k);
// 返回头结点指针
return previous;
}
复制代码
指针保存的是变量的内存地址,经过指针就能找到这个变量,如链表中常常写的 p->next = q,意思是说 p 结点的next 指针保存了 q 结点的内存地址。
对于链表的插入操做,须要注意代码的前后顺序,写反了就会发生内存泄漏,删除操做须要手动 free 结点的内存
。
// 在 a 结点和 b 结点中间插入 x 结点,p 指针指向 a 结点, 正确写法✔️
x->next = p->next->next;
p->next = x;
// 错误写法,这个时候 p->next 已经不指向 b 了,而是指向 x。❌
p->next = x;
x->next = p->next;
复制代码
有时候处理头指针和尾指针是须要特别的处理,代码也不统一,引入哨兵结点
后,能够简化问题,哨兵结点
不存储数据,只是 next 指针指向链表的实际结点,这样,头结点的逻辑就能够和其余结点同样了,不用特别处理。这样就简化了代码逻辑。如链表的插入逻辑和删除逻辑,不用哨兵结点的话,就须要区别对待头结点和尾结点。引入哨兵结点的链表叫作带头链表
,相反没有哨兵结点的链表叫作不带头链表
。如图所示:
写链表代码很容易出错,须要考虑的边界条件有不少,有些额外须要作特别处理,须要特别考虑如:
若是空间想象能力不够好,特别是多层循环或者递归时,画图辅助分析能够帮助定位每一步的变量值,指针是怎么指向的,也能够在没有思路的时候经过画图辅助分析一步一步的总结概括出规律来,这个时候算法思路就变清晰一些。我写快慢指针检测环,定位链表中点和两两翻转链表代码时就是在笔记上一步一步推到出代码规律来的。
不少链表相关的写法其实写多了会发现不少相似的思路,如快慢指针思路,既能够用来定位链表中间结点,又能够用来检测环,复杂点的问题也通常能够分割成小问题来处理。
扩展阅读
分享我的技术学习记录和跑步马拉松训练比赛、读书笔记等内容,感兴趣的朋友能够关注个人公众号「青争哥哥」。