文章目录
一、力扣206.反转链表
- 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
- 示例1:
- 示例2:
- 示例3:
(一)不带头节点的单链表原地反转
- 单链表结构:
struct ListNode
{
int val;
struct ListNode* next;
}ListNode;
- 思路如下:
- 代码如下:
struct ListNode* reverseList(struct ListNode* head)
{
if (head == nullptr || head->next == nullptr) return head;
struct ListNode* pre = nullptr;
struct ListNode* cur = head;
struct ListNode* next = nullptr;
while (cur != nullptr)
{
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
return pre;
}
(二)利用栈的先进后出特点
- 由于栈有先进后出的特点,因此我们可以在不破坏原链表的情况下,将链表的节点放进栈里面,然后进行反转。
- 和原地反转不同的是,这种方法需要以一个栈为代价,但是不会破坏单链表的原始结构。
- 在面试中我们应该尽量问清楚面试官对题目的要求,特别是时间和空间复杂度。
- 思路是比较简单的,因此我们直接看代码:
struct ListNode* reverseList(struct ListNode* head)
{
if (head == nullptr || head->next == nullptr)
{
return head;
}
struct ListNode* phead = head;
stack<int> st;
while (phead != nullptr)
{
st.push(phead->val);
phead = phead->next;
}
phead = head;
while (!st.empty())
{
phead->val = st.top(); st.pop();
phead = phead->next;
}
return head;
}
(三)递归(空间复杂度O(n))
- 递归和栈的思想有点类似,利用栈是因为栈的先进后出的特点,而递归其实分为两个过程----调用和回退,因此我们可以在递归回退的过程中解决反转这个问题。
- 思路如下:
- 代码如下:
struct ListNode* reverseList(struct ListNode* head)
{
if (!head || !head->next)
{
return head;
}
struct ListNode* newHead = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return newHead;
}
二、力扣92.反转链表 II
- 给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表。
- 输入输出示例:
- 提示:
(一)穿针引线----理清思路再编码
- 思路如下图所示:
- 首先考虑到left位置可能位于头节点,所以我们先开辟一个伪头节点出来;
- 接下来我们要找到left位置的前一个节点和right位置的后一个节点并保存下来;
- 然后我们需要找到要反转的区间链表;
- 找到之后把这段区间和原来的链表断开;
- 将这段区间进行链表反转;
- 反转完成之后再和原来的链表进行合并。
- 代码如下:
struct ListNode {
int val;
ListNode* next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode* next) : val(x), next(next) {}
};
//反转链表
void reverse(ListNode* head)
{
struct ListNode* cur = head;
struct ListNode* pre = nullptr;
struct ListNode* next = nullptr;
while (cur != nullptr)
{
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
}
ListNode* reverseBetween(ListNode* head, int left, int right)
{
if (head == nullptr || head->next == nullptr || right == left)
{
return head;
}
//自定义一个头节点,处理left在头节点位置的情况
ListNode* vir = new ListNode(-1);
vir->next = head;
//先找到left位置的前一个节点
ListNode* leftpre = vir;
for (int i = 0; i < left - 1; i++)
{
leftpre = leftpre->next;
}
//然后走right - left +1步找到right位置的节点
ListNode* rightNode = leftpre;
for (int i = 0; i < right - left + 1; i++)
{
rightNode = rightNode->next;
}
//截取链表
ListNode* leftNode = leftpre->next;
ListNode* rightsuc = rightNode->next;
//切断链表
leftpre->next = nullptr;
rightNode->next = nullptr;
//反转leftNode~rightNode之间的链表
reverse(leftNode);
//合并链表
leftpre->next = rightNode;
leftNode->next = rightsuc;
return vir->next;
}
(二)一次遍历反转链表----头插法
- 参考力扣官方题解,特别详细~很容易懂
- 假设有下面这样一个链表:
- 整体思想是:在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。下面的图展示了整个流程。
- 下面我们具体解释如何实现:
- 使用三个指针变量 pre、curr、next 来记录反转的过程中需要的变量,它们的意义如下:
- curr:指向待反转区域的第一个节点 left;
- next:永远指向 curr 的下一个节点,循环过程中,curr 变化以后 next 会变化;
- pre:永远指向待反转区域的第一个节点 left 的前一个节点,在循环过程中不变。
- 第 1 步,我们使用 ①、②、③ 标注「穿针引线」的步骤。
- 先将 curr 的下一个节点记录为 next;
- 执行操作 ①:把 curr 的下一个节点指向 next 的下一个节点;
- 执行操作 ②:把 next 的下一个节点指向 pre 的下一个节点;
- 执行操作 ③:把 pre 的下一个节点指向 next。
- 第 1 步完成以后「拉直」的效果如下:
- 同理,第二步:
- 第三步,同理
- 代码如下:
ListNode* reverseBetween(ListNode* head, int left, int right)
{
if (head == nullptr || head->next == nullptr || left == right)
{
return head;
}
//构造一个伪节点
ListNode* vir = new ListNode(-1);
vir->next = head;
//找到left位置的前一个节点
ListNode* pre = vir;
for (int i = 0; i < left - 1; i++)
{
pre = pre->next;
}
//找到反转区间的第一个节点
ListNode* cur = pre->next;
//left~right区间内共需要反转right - left次
for (int i = 0; i < right - left; i++)
{
ListNode* next = cur->next;
cur->next = next->next;
next->next = pre->next;
pre->next = next;
}
return vir->next;
}
三、剑指 Offer 06.从尾到头打印链表
- 输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
- 输入:head = [1,3,2]
- 输出:[2,3,1]
(一)利用链表反转解决
- 思路比较简单,就是先将链表反转,然后将该链表从头到尾遍历,将节点的val值依次放入vector中。
- 代码:
//链表原地反转
struct ListNode* reverseList(struct ListNode* head)
{
if (head == nullptr || head->next == nullptr) return head;
struct ListNode* pre = nullptr;
struct ListNode* cur = head;
struct ListNode* next = nullptr;
while (cur != nullptr)
{
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
return pre;
}
vector<int> reversePrint(ListNode* head)
{
vector<int> res;
if (head == nullptr)
{
return res;
}
struct ListNode* phead = reverseList(head);
while (phead != nullptr)
{
res.push_back(phead->val);
phead = phead->next;
}
return res;
}
(二)递归
- 在回退的过程中将节点中的val值依次装入vector中。
- 代码如下:
vector<int> reversePrint(ListNode* head)
{
vector<int> res;
if (head == nullptr)
{
return res;
}
res = reversePrint(head->next);
res.push_back(head->val);
return res;
}
四、力扣25.k个一组反转链表
- 给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
- k 是一个正整数,它的值小于或等于链表的长度。
- 如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
- 进阶:
- 你可以设计一个只使用常数额外空间的算法来解决此问题吗?
- 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
- 输入输出示例:
- 提示:
(一)利用栈进行辅助【直接修改节点的val值】
- 这种解法思路比较简单,也比较好想:
- 先计算出所有节点的个数;
- 然后通过所有节点的个数和k的值计算共需要反转的组数;
- 然后将每组的k个节点的val值装进栈中,然后再出栈覆盖掉原来的k个节点的val值;
- 题目完成。
- 代码如下:
ListNode* reverseKGroup(ListNode* head, int k)
{
if (head == nullptr || head->next == nullptr || k == 1)
{
return head;
}
//先计算出链表中的所有节点数
int count = 0;
ListNode* phead = head;
while (phead != nullptr)
{
count++;
phead = phead->next;
}
//计算出共需要反转的组数
int tmp = count / k;
//还需要一个栈来存储需要反转的每组节点的val值
stack<int> st;
phead = head;
ListNode* list = head;
//共有tmp组需要进行反转操作
for (int i = 0; i < tmp; i++)
{
//将每组的k个节点入栈
for (int j = 0; j < k; j++)
{
if (phead != nullptr)
{
st.push(phead->val);
phead = phead->next;
}
}
//将栈中的val出栈(进行反转)
while (!st.empty())
{
list->val = st.top(); st.pop();
list = list->next;
}
}
return head;
}
(二)原链表上进行反转【头插法】
- 思路用下图简单说明:
- 代码如下:
ListNode* reverseKGroup(ListNode* head, int k)
{
if (head == nullptr || head->next == nullptr || k == 1)
{
return head;
}
//先计算出链表中的所有节点数
int count = 0;
ListNode* phead = head;
while (phead != nullptr)
{
count++;
phead = phead->next;
}
//计算出共需要反转的组数
int tmp = count / k;
//定义一个伪头节点
ListNode* vir = new ListNode(-1);
ListNode* pre = vir;
ListNode* cur = head;
ListNode* next = nullptr;
vir->next = head;
//共有tmp组需要进行反转操作
for (int i = 0; i < tmp; i++)
{
for (int j = 0; j < k - 1; j++)
{
if (cur->next != nullptr)
{
next = cur->next;
cur->next = next->next;
next->next = pre->next;
pre->next = next;
}
}
pre = cur;
cur = cur->next;
}
return vir->next;
}
五、力扣61.旋转链表
- 给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
- 输入输出示例:
- 提示:
(一)利用数学思想将题目简化
- 通过题目我们可以分析出来这样一个信息:
- 有了上面这样一个思路,代码写起来就很简单啦:
ListNode* rotateRight(ListNode* head, int k)
{
if (head == nullptr || head->next == nullptr || k == 0)
{
return head;
}
//先计算出节点的个数
int count = 0;
ListNode* newhead = head;
while (newhead != nullptr)
{
count++;
newhead = newhead->next;
}
//当k的值大于节点的个数时,把循环除去
int cnt = k % count;
//考虑到cnt==0时直接将原链表的头返回即可
if (cnt == 0)
{
return head;
}
//找到第cnt个节点的前一个节点
newhead = head;
for (int i = 1; i < count - cnt; i++)
{
if (newhead->next != nullptr)
{
newhead = newhead->next;
}
}
//找到第cnt个节点
ListNode* phead = newhead->next;
//将链表从第cnt节点这断开
newhead->next = nullptr;
//找到链表的尾部
ListNode* tail = phead;
while (tail->next != nullptr)
{
tail = tail->next;
}
//将head连接到tail的尾部
tail->next = head;
return phead;
}
六、剑指Offer 22.链表中倒数第k个节点
- 输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
- 例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
- 输入输出示例:
- 给定一个链表: 1->2->3->4->5, 和 k = 2.
- 返回链表 4->5.
(一)遍历统计链表长度
- 先遍历统计链表长度,记为 n ;
- 设置一个指针走(n−k) 步,即可找到链表倒数第 k 个节点。
- 代码如下:
ListNode* getKthFromEnd(ListNode* head, int k)
{
ListNode* phead = head;
int count = 0;
while (phead != nullptr)
{
count++;
phead = phead->next;
}
int n = count - k;
phead = head;
while (n > 0)
{
phead = phead->next;
n--;
}
return phead;
}
旺财加油!✨