链表
1. 链表反转
1.1 整个链表反转(#206)
题目描述:
反转一个单链表。
示例:
输入:
1
−
>
2
−
>
3
−
>
4
−
>
5
−
>
N
U
L
L
1->2->3->4->5->NULL
1−>2−>3−>4−>5−>NULL
输出:
5
−
>
4
−
>
3
−
>
2
−
>
1
−
>
N
U
L
L
5->4->3->2->1->NULL
5−>4−>3−>2−>1−>NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
1.1.1 迭代循环实现
代码-C
1.1.2 递归实现
代码-C
/* 递归实现 */
struct ListNode* reverseList(struct ListNode* head){
if(head == NULL || head->next == NULL)
return head;
struct ListNode *cur = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return cur;
}
1.2 反转单链表前k个节点
//反转前K个链表
//反转某一部分链表可以视为是把链表分为了几个子链表,反转指定子链表后再把这些子链表拼接起来
struct ListNode* reverseListFrontK(struct ListNode* head, int k)
{
//保存第K+1个结点
struct ListNode *temp = head;
for(int i=1; i <= k; i++)
{
printf("i=%d, %d \n", i, temp->data);
temp = temp->next;
}
// 反转前K个结点
struct ListNode* pre = NULL;
struct ListNode* cur = head;
struct ListNode* next = NULL;
while(cur != temp)
{
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
// 此时pre指向第k个结点,cur不满足条件指向k+1个结点
// 反转指定子链表后再把这些子链表拼接起来
// head变为新子链表尾结点,指向k+1个结点拼接
head->next = temp;
return pre;
}
1.4 反转单链表后K个结点
// 反转单链表后K个结点
struct ListNode* reverseListTailK1(struct ListNode* head, int k)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
for (int i = 1; i < k+1; i++)
{
//i=1, fast指向第二个结点
//i=k,fast指向第k+1个结点,结束循环
fast = fast->next;
}
while (fast->next != NULL) //尾结点
{
fast = fast->next;
slow = slow->next;
}
// slow指向k+1个结点
// 反转倒数k个结点组成的子链表
struct ListNode* pre = NULL;
struct ListNode* cur = slow->next; //指向第k个结点
struct ListNode* next = NULL;
while (cur != NULL)
{
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
// 此时pre指向原链表尾结点,他是新子链表的首节点
slow->next = pre;
return head;
}
1.5 反转单链表部分结点
struct ListNode* reverseListBetweenL2R(struct ListNode* head, int L, int R)
{
if (head == NULL || R < L || L < 1)
return head;
struct ListNode* temp = head;
struct ListNode* p = NULL;
struct ListNode* q = NULL;
int len = 0;
// p保存第L-1个结点 和 q保存第R+1个结点
while (temp) {
len++;
p = (len == L - 1 ? temp : p);
q = (len == R + 1 ? temp : q);
temp = temp->next;
}
if (R > len)
return head;
// 反转倒数k个结点组成的子链表
struct ListNode* pre = NULL;
struct ListNode* cur = (p == NULL ? head : p->next);; //指向第L个结点
struct ListNode* next = NULL;
while (cur != q)
{
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
if (p != NULL)
{
p->next = pre;
q->next = cur;
return head;
}
else // L=1的情况
{
head->next = cur;
return pre;
}
}
1.6 反转单链表的相邻接点
// 反转单链表的相邻接点
struct ListNode* reverseListInPairs(struct ListNode* head)
{
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
dummy->next = head;
dummy->data = -1;
struct ListNode* cur = dummy;
while (cur->next != NULL && cur->next->next != NULL)
{
struct ListNode* node1 = cur->next;
struct ListNode* node2 = cur->next->next;
node1->next = node2->next; // 当前子链表“尾节点”指向下一个子链表的首结点
node2->next = node1;// 处理当前子链表
cur = node1;// 更新cur游标
}
return dummy->next;
}
1.7 链表中的节点每k个一组翻转
力扣中国
因为可能会处理到首节点,所以需要新增一个假节点。
// 链表中的节点每k个一组翻转
struct ListNode * reverseKGroup(struct ListNode* head, int k) {
//先创建一个哑节点
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
//让哑节点的指针指向链表的头
dummy->next = head;
//pre 指向子链表首节点的前一个节点, end指向子链表尾结点
struct ListNode* pre = dummy;
struct ListNode* end = dummy;
while (end->next != NULL) {
//每k个反转,end是每k个链表的最后一个
for (int i = 0; i < k && end != NULL; i++)
end = end->next;
//如果end是空,说明不够k个,就不需要反转了,直接退出循环。
if (end == NULL)
break;
// start指向子链表首节点
struct ListNode* start = pre->next;
// next是下一组子链表的首节点
struct ListNode* next = end->next;
// 子链表和后面的子链表断开
end->next = NULL;
// 因为pre是反转链表的前一个节点,我们把小链表[start,end]
// 反转之后,让pre的指针指向这个反转的小链表
pre->next = reverseList(start);
// 注意经过上一步反转之后,start反转到链表的尾部了,就是已经
// 反转之后的尾结点了,让他指向下一次反转的头结点即可
start->next = next;
// 前面反转完了,要进入下一波了,pre和end都有重新赋值
pre = start; // 因为要记录前一个节点,所以更新赋值为start(上个子链表的尾结点)
end = start;
}
return dummy->next;
}
2. 找出两个链表的交点(#160)
题目描述:
例如以下示例中 A 和 B 两个链表相交于 c1:
这里注意,相求的是相交的链表部分,而不仅仅是节点的值相等,地址也应该是相同的;
A: a1 → a2
↘
c1 → c2 → c3
↗
B: b1 → b2 → b3
但是不会出现以下相交的情况,因为每个节点只有一个 next 指针,也就只能有一个后继节点,而以下示例中节点 c 有两个后继节点。
A: a1 → a2 d1 → d2
↘ ↗
c
↗ ↘
B: b1 → b2 → b3 e1 → e2
要求时间复杂度为 O ( N ) O(N) O(N),空间复杂度为 O ( 1 ) O(1) O(1)。如果不存在交点则返回 null。
设 A 的长度为 a + c a + c a+c,B 的长度为 b + c b + c b+c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a a + c + b = b + c + a a+c+b=b+c+a。
当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。
如果不存在交点,那么 a + b = b + a a + b = b + a a+b=b+a,以下实现代码中 l 1 l1 l1 和 l 2 l2 l2 会同时为 n u l l null null,从而退出循环。
代码-C
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode *p = headA;
struct ListNode *q = headB;
// 假设A为:A1 A2 A3 C1 C2
// 假设B为:B1 B2 C1 C2
// A1 A2 A3 C1 C2 B1 B2 C1 C2
// B1 B2 C1 C2 A1 A2 A3 C1 C2
// ^
while(p != q)
{
p = (p==NULL) ? headB : p->next;
q = (q==NULL) ? headA : q->next;
}
return p;
}
3. 归并两个有序的链表
3.1 迭代循环实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2)
{
struct ListNode* dummy = malloc(sizeof(struct ListNode)); /* 申请一个头结点,作为新链表 */
struct ListNode* p; /* 新链表的指针 */
dummy->next = NULL; /* 新链表初始化 */
p = dummy;
while (l1 && l2) {
if (l1->val < l2->val) { /* 当前l1的结点值比l2的小 */
p->next = l1;
l1 = l1->next;
}
else { /* 当前l2的结点值比l1的小 */
p->next = l2;
l2 = l2->next;
}
p = p->next;
}
/* 剩余结点直接链接在新链表后 */
if (l1)
p->next = l1;
if (l2)
p->next = l2;
return dummy->next;
}
3.2 递归实现
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
4. 删除链表节点
4.1 从有序链表中删除重复节点:仅保留一个重复元素
描述
删除给出链表中的重复元素(链表中元素从小到大有序),使链表中的所有元素都只出现一次
例如:
给出的链表为1→1→2返回1→2
给出的链表为1→1→2→3→3返回1→2→3.
struct ListNode* deleteDuplicates1(struct ListNode* head) {
if (!head || head->next == NULL) //空链表或者只有一个节点的链表直接返回
return head;
struct ListNode* cur = head; //引入一个当前指针指向链表的头结点
while (cur->next)
{
if (cur->val == cur->next->val) //判断当前节点是否与下一个节点的值相等,相等则直接删除下一个节点
cur->next = cur->next->next;
else //当前节点与下一个节点的值不相等,则直接滑动指针
cur = cur->next;
}
return head;
}
4.2 从有序链表中删除重复节点:删除所有重复元素
描述
给出一个升序排序的链表,删除链表中的所有重复出现的元素,只保留原链表中只出现一次的元素。
例如:
给出的链表为1→2→3→3→4→4→5, 返回1→2→5.
给出的链表为1→1→1→2→3, 返回2→3.
示例1
输入:{1,2,2}
返回值:{1}
因为可能会处理到首节点,所以需要新增一个假节点。
struct ListNode* deleteDuplicates2(struct ListNode* head) {
struct ListNode* dummp = (struct ListNode*)malloc(sizeof(struct ListNode));
dummp->next = head; //空节点插入链表头部
struct ListNode* p = dummp;
while (p->next && p->next->next)
{ //每次都是2个节点判断
if (p->next->val == p->next->next->val)
{ //存在重复节点
int num_flag = p->next->val; //记录第一个重复节点的值
while (p->next && p->next->val == num_flag) //循环遍历后续节点的值,并与记录的节点值比较,相同则逐个删除
p->next = p->next->next;
}
else //本轮不存在重复节点,值链表指针后移
p = p->next;
}
return dummp->next; //返回结果
}
4.3 删除链表的倒数第n个节点
双指针法,因为会处理到头节点,所以需要新增一个 假节点。
#include <stdio.h>
#include <stdlib.h>
struct ListNode* removeNthFromEnd(struct ListNode* head, int n ) {
if (head == NULL) //特殊情况,空链表
return head;
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode)); //新建一个头结点
dummy->next = head; //新头结点指针指向原链表头结点,其实是第一个元素
struct ListNode* fast = dummy; //新建快指针指向头结点
for (int i = 0; i < n; i++)
fast = fast->next; //快指针先走n步停下
printf("fast: %d\n", fast->val);
struct ListNode* slow = dummy; //新建慢指针,此时两者相差n
while (fast->next != NULL)
{
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
return dummy->next; //返回的链表不需要头结点,直接从第一个元素值开始
}
5. 合并两个有序链表
6. 单链表的排序
用到排序算法,暂时不看
知识点1:分治
分治即“分而治之”,“分”指的是将一个大而复杂的问题划分成多个性质相同但是规模更小的子问题,子问题继续按照这样划分,直到问题可以被轻易解决;“治”指的是将子问题单独进行处理。经过分治后的子问题,需要将解进行合并才能得到原问题的解,因此整个分治过程经常用递归来实现。
知识点2:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
7. 链表求和
7.1 链表求和1
最高位在链表首结点
#include <stdlib.h>
struct ListNode* addInList(struct ListNode* head1, struct ListNode* head2 ) {
// write code here
if(head1 == NULL) return head2;
if(head2 == NULL) return head1;
struct ListNode* dummy = malloc(sizeof(struct ListNode));
struct ListNode* resultList = dummy;
int sum = 0;
while(head1 != NULL || head2 != NULL)
{
if(head1 != NULL)
{
sum += head1->val;
head1 = head1->next;
}
if(head2 != NULL)
{
sum += head2->val;
head2 = head2->next;
}
// 创建新节点后
struct ListNode* tempNode = malloc(sizeof(struct ListNode));
tempNode->val = sum % 10;
resultList->next = tempNode;
resultList = resultList->next;
sum = sum / 10; // 进位累加到下一位计算
}
// 最高位进位处理
if(sum != 0)
{
struct ListNode* tempNode = malloc(sizeof(struct ListNode));
tempNode->val = sum % 10;
resultList->next = tempNode;
}
return dummy->next;
}
7.2 链表求和2
BM11 链表相加(二)
最高位在链表尾结点
#include <stdlib.h>
struct ListNode* revertList(struct ListNode* head)
{
struct ListNode* pre = NULL;
struct ListNode* cur = head;
struct ListNode* next = NULL;
while(cur != NULL)
{
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
return pre;
}
struct ListNode* addInList(struct ListNode* head1, struct ListNode* head2 ) {
// write code here
// 方式一:C语言使用把两个链表反转,再按照链表相加(一)方法相加,然后再将结果链表反转
// 方式二:C++语言使用出栈入栈来实现链表的反转
// 方式一
if(head1 == NULL) return head2;
if(head2 == NULL) return head1;
struct ListNode* rhead1 = revertList(head1);
struct ListNode* rhead2 = revertList(head2);
struct ListNode* dummy = malloc(sizeof(struct ListNode));
struct ListNode* resultList = dummy;
int sum = 0;
int cnt = 0;
while(rhead1 != NULL || rhead2 != NULL)
{
if(rhead1 != NULL)
{
sum += rhead1->val;
rhead1 = rhead1->next;
}
if(rhead2 != NULL)
{
sum += rhead2->val;
rhead2 = rhead2->next;
}
// 创建新节点后
struct ListNode* tempNode = malloc(sizeof(struct ListNode));
tempNode->val = sum % 10;
resultList->next = tempNode;
resultList = resultList->next;
sum = sum / 10; // 进位累加到下一位计算
}
// 最高位进位处理
if(sum != 0)
{
struct ListNode* tempNode = malloc(sizeof(struct ListNode));
tempNode->val = sum % 10;
resultList->next = tempNode;
}
return revertList(dummy->next);
}
8. 回文链表
9. 分隔链表
10. 链表中环的入口结点
描述
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
链表中环的入口结点
这题我们可以采用双指针解法,一快一慢指针。快指针每次跑两个element,慢指针每次跑一个。如果存在一个圈,总有一天,快指针是能追上慢指针的。
如下图所示,我们先找到快慢指针相遇的点,p。我们再假设,环的入口在点q,从头节点到点q距离为A,q p两点间距离为B,p q两点间距离为C。
因为快指针是慢指针的两倍速,且他们在p点相遇,则我们可以得到等式 2(A+B) = A+B+C+B. (感谢评论区大佬们的改正,此处应为:如果环前面的链表很长,而环短,那么快指针进入环以后可能转了好几圈(假设为n圈)才和慢指针相遇。但无论如何,慢指针在进入环的第一圈的时候就会和快的相遇。等式应更正为 2(A+B)= A+ nB + (n-1)C)
由3的等式,我们可得,C = A。
这时,因为我们的slow指针已经在p,我们可以新建一个另外的指针,slow2,让他从头节点开始走,每次只走下一个,原slow指针继续保持原来的走法,和slow2同样,每次只走下一个。
我们期待着slow2和原slow指针的相遇,因为我们知道A=C,所以当他们相遇的点,一定是q了。
我们返回slow2或者slow任意一个节点即可,因为此刻他们指向的是同一个节点,即环的起始点,q。
// 判断是否有环
struct ListNode* EntryNodeOfLoop(struct ListNode* pHead)
{
if (pHead == NULL || pHead.next == NULL)
{
return NULL;
}
struct ListNode* fast = pHead;
struct ListNode* slow = pHead;
while (fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
struct ListNode* slow2 = pHead;
while (slow2 != slow) {
slow2 = slow2->next;
slow = slow->next;
}
return slow2;
}
}
return NULL; // 无环
}