【算法】—— 链表常见算法2
1. 链表的中间结点
1.1 问题描述
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:[1,2,3,4,5]
输出:此列表中的结点 3
示例 2:
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4
1.2 实现思路
使用快慢指针实现,遍历链表,快指针走两步,慢指针走一步,快指针遍历完,慢指针的位置就是中间结点的位置
1.3 代码实现
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
//fast指针或fast的下一个为NULL
while (fast!=NULL && fast->next != NULL)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
2. 链表中倒数第k个结点
2.1 问题描述
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
2.2 实现思路
利用快慢指针,快指针先走k步,然后快慢指针一起走,快指针遍历完,慢指针的位置就是倒数第k个
- 快指针先走k步
- 快慢指针一起走,直到快指针遍历完,返回慢指针
2.3 代码实现
struct ListNode* getKthFromEnd(struct ListNode* head, int k)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
while (k--) //先走k-1步
{
fast = fast->next;
}
while (fast != NULL)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
3. 链表分割
3.1 问题描述
现有一链表的头指针ListNode* pHead
,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
示例
输入:5 {3 6 8 2 4 9 7 1}
输出:3 2 4 1 6 8 9 7
3.2 实现思路
方法一(先分类再合并)
- 创建两个链表头指针,一个用来放小于x的位于前面的结点,一个用来放大于等于x的结点
- 最后将两个链表合并到一起
ListNode* partition(struct ListNode* pHead, int x)
{
struct ListNode* cur = pHead;
struct ListNode h1 = { 0 };
struct ListNode h2 = { 0 };
struct ListNode* p1 = &h1;
struct ListNode* p2 = &h2;
while (cur != NULL)
{
if (cur->data < x) //收集小于x的结点的第一个链表
{
p1->next = cur;
p1 = p1->next;
}
else //收集大于等于x的结点的第二个链表
{
p2->next = cur;
p2 = p2->next;
}
cur = cur->next;
}
//拼接两个链表
p2->next = NULL; //第二个链表末尾置空
p1->next = h2.next;
pHead = h1.next;
return pHead;
}
方法二(原地插入)
遍历链表,将小于x的结点插入到链表前面
-
先为链表插入头结点,指针
slow
作为慢指针始终指向小于x的结点要插入的位置 -
fast
作为快指针进行遍历数组,pre
和fastnext
分别指向fast
指针的前后位置 -
当
fast
指向的结点小于x 且slow
与pre
指针指向同一个位置时,无需插入操作所有指针统一向下一位移动per == slow
时pre
的next
改变会影响slow
的next
的指向
- 当
fast
指向的结点大于x时,要将fast
、pre
、fastnext
向后移动继续遍历,slow
不变,等待小于x的结点插入
- 当
fast
指向的结点小于x 且slow
与pre
指针指向不同位置时,将fast
指向的结点插入到slow
后边,所有指针后移,除了pre
- 插入的方式改变
next
的连接就可以,一共三条线 fast
指向的结点被插入到前面,pre
指针用来连接fast
后面的链表,fast
向后递增,所以此时pre
指向的结点依然作为fast
的前一个结点
- 插入的方式改变
代码实现
ListNode* partition(ListNode* pHead, int x)
{
if (pHead == NULL || pHead->next == NULL)
{
return pHead;
}
ListNode temp = { 0 };
temp.next = pHead;
ListNode* slow = &temp;
ListNode* pre = &temp;
ListNode* fast = pHead;
ListNode* fastnext = pHead;
while (fast != NULL)
{
fastnext = fast->next;
if (fast->data < x) //小于x的结点
{
if (pre != slow) //pre与slow不重合时才插入
{
pre->next = fastnext;
fast->next = slow->next;
slow->next = fast;
}
else //重合时不插入,所有结点向后递增
{
pre = pre->next;
}
slow = slow->next;
}
else //大于等于x的结点不做处理,除了slow其余指针向后递增
{
pre = pre->next;
}
fast = fastnext;
}
pHead = temp.next;
return pHead;
}
4. 回文链表
4.1 问题描述
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
4.2 实现思路
- 获取该链表的中间结点,调用中间结点的函数(本文第一个算法)
- 逆置后半段链表,调用反转单链表函数(点击跳转),由于前半段指针指向没有改变,所以2依然指向3
- 比较两个链表的数值,直到一方遍历结束则是回文链表(实际上该3结点是两个链表公用的)
4.3 代码实现
//获取中间结点
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
//fast指针或fast的下一个为NULL
while (fast!=NULL && fast->next != NULL)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
//反转单链表
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* prev = NULL;
struct ListNode* cur = head;
struct ListNode* next = NULL;
while (cur != NULL)
{
next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
//判断回文链表
bool isPalindrome(struct ListNode* head)
{
struct ListNode* mid = middleNode(head);
struct ListNode* rev = reverseList(mid);
while (rev != NULL && head != NULL)
{
if (rev->val != head->val)
{
return false;
}
head = head->next;
rev = rev->next;
}
return true;
}