目录
数据结构与算法
面试注意点
练习
链表
基本技能
常见题型
83. 删除排序链表中的重复元素:remove-duplicates-from-sorted-list
82. 删除排序链表中的重复元素 II remove-duplicates-from-sorted-list-ii
206. 反转链表:reverse-linked-list
剑指 Offer 24. 反转链表
92. 反转链表 II:reverse-linked-list-ii
25. K 个一组翻转链表
21. 合并两个有序链表merge-two-sorted-lists
剑指 Offer 25 合并两个排序的链表
86. 分隔链表:partition-list
148. 排序链表sort-list 复杂
143. 重排链表reorder-list
141. 环形链表linked-list-cycle
142 环形链表 Ⅱ linked-list-cycle-ii
剑指offer 牛客
138. 复制带随机指针的链表copy-list-with-random-pointer
剑指 Offer 35. 复杂链表的复制
剑指offer
剑指 Offer 06. 从尾到头打印链表
剑指 Offer 18. 删除链表的节点
剑指 Offer 22. 链表中倒数第k个节点
总结
练习
1. 两数之和
方法二:哈希表
2. 两数相加
160 相交链表(easy)
剑指 Offer 52. 两个链表的第一个公共节点
19 删除链表的倒数第N个结点(medium)
203 移除链表元素(easy)
328 奇偶链表
430 扁平化多级双向链表 还没看
61 旋转链表(medium)
数据结构是一种数据的表现形式,如链表、二叉树、栈、队列等都是内存中一段数据表现的形式。 算法是一种通用的解决问题的模板或者思路,大部分数据结构都有一套通用的算法模板,所以掌握这些通用的算法模板即可解决各种算法问题。
后面会分专题讲解各种数据结构、基本的算法模板、和一些高级算法模板,每一个专题都有一些经典练习题,完成所有练习的题后,你对数据结构和算法会有新的收获和体会。
先介绍两个算法题,试试感觉~
示例 1
strStr
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从 0 开始)。如果不存在,则返回 -1。
思路:核心点遍历给定字符串字符,判断以当前字符开头字符串是否等于目标字符串
需要注意点
循环时,i 不需要到 len-1
如果找到目标字符串,len(needle)==j
示例 2
subsets
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
思路:这是一个典型的应用回溯法的题目,简单来说就是穷尽所有可能性,算法模板如下
通过不停的选择,撤销选择,来穷尽所有可能性,最后将满足条件的结果返回
说明:后面会深入讲解几个典型的回溯算法问题,如果当前不太了解可以暂时先跳过
我们大多数时候,刷算法题可能都是为了准备面试,所以面试的时候需要注意一些点
快速定位到题目的知识点,找到知识点的通用模板,可能需要根据题目特殊情况做特殊处理。
先去朝一个解决问题的方向!先抛出可行解,而不是最优解!先解决,再优化!
代码的风格要统一,熟悉各类语言的代码规范。
命名尽量简洁明了,尽量不用数字命名如:i1、node1、a1、b2
常见错误总结
访问下标时,不能访问越界
空值 nil 问题 run time error
strStr
subsets
链表相关的核心点
null/nil 异常处理
dummy node 哑巴节点
快慢指针
插入一个节点到排序链表
从一个链表中移除一个节点
翻转链表
合并两个链表
找到链表的中间节点
83. 删除排序链表中的重复元素:模拟题
,直接遍历链表,遇到重复值的节点删除即可。
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
/*解法一:递归
主要思想:
链表具有天然的递归性,可以把一个链表看成头节点后挂接一个更短的链表,
这个更短的链表看成其的头节点后面挂接一个更更短的链表,依次类推。
所以可以先处理头节点后面挂接的更短的链表,处理完之后,
判断头节点的值是否等于其挂接的更短的链表的头节点的值,
如果相等则直接返回更短的链表的头节点,
否则返回原链表的头节点。
边界条件
1、 链表为空;
2、 链表只有头节点。
*/
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (head == NULL || head->next == NULL) {
return head;
}
// 删除头节点后面挂接的链表中的重复元素
head->next = deleteDuplicates(head->next);
// 头节点与后面挂接的链表中的节点值相同,则头节点也删除,否则不删除
return head->val == head->next->val ? head->next : head;
}
};
/*解法二:直接法(单指针)
由于题目已经明确说了是排序链表,所以只要挨个比较相邻节点的值是否相等就可以了,相等就通过将当前节点指向其后继节点的后继节点去删除后继节点,否则继续查找。*/
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (head == NULL || head->next == NULL) { // 特判
return head;
}
ListNode* cur = head; // 当前节点
while (cur != NULL && cur->next != NULL) { // 一直遍历,直到当前节点为空或者当前节点是尾节点
if (cur->val == cur->next->val) { // 当前节点的值与其后继节点的值相同,则删除当前节点的后继节点
cur->next = cur->next->next;
} else { // 否则继续遍历
cur = cur->next;
}
}
return head;
}
};
/*解法二:直接法(双指针)
和leetcode 26. 删除排序数组中的重复项 思路一样
*/
和
ListNode deleteDuplicates(ListNode head) {
if (head == null) return null;
ListNode slow = head, fast = head;
while (fast != null) {
if (fast.val != slow.val) {
// nums[slow] = nums[fast];
slow.next = fast;
// slow++;
slow = slow.next;
}
// fast++
fast = fast.next;
}
// 断开与后面重复元素的连接
slow.next = null;
return head;
}
82. 删除排序链表中的重复元素 II:模拟题
,遍历链表,若head的节点值与head的next节点值不相等,则pre指向head,也就是不重复节点;若相等,我们需要找到重复值子链表的最后一个节点,然后令pre指向head->next,同时head移动到下一个节点。
思路:链表头结点可能被删除,所以用 dummy node 辅助删除
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现的数字。
示例 1:
输入: 1->2->3->3->4->4->5
输出: 1->2->5
示例 2:
输入: 1->1->1->2->3
输出: 2->3
方法一:迭代。先放C++代码,思路清晰明了。
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/solution/zi-jie-ti-ku-82-zhong-deng-shan-chu-pai-xu-lian-bi/
/**
* Definition for singly-linked list.
* 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) {}
* };
*/
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (head==NULL ||head->next==NULL){
return head;
}
ListNode *dummy=new ListNode(INT_MAX);
dummy->next=head;
ListNode *a=dummy;
ListNode *b=head;
while (b && b->next)
{
if (a->next->val!=b->next->val)
{
a=a->next;
b=b->next;
}
else
{
while(b && b->next && a->next->val==b->next->val)
{
b=b->next;
}
a->next=b->next;
b=b->next;
}
}
return dummy->next;
}
};
方法二:递归。先放C++代码,思路清晰明了。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if(!head || !head->next) return head;
if(head->next->val != head->val)
{
// 如果head后面一位的元素的值不等于head的值,就从head->next开始接着处理后面的链表
head -> next = deleteDuplicates(head -> next);
return head;
}
else
{
// 如果head后面一位的元素的值等于head的值,就跳过所有与head的值相等的元素,从第一个不等于head值的元素开始处理
ListNode *curr = head;
while(curr->next && curr->next->val==curr->val) curr = curr -> next;
return deleteDuplicates(curr -> next);
}
}
};
206. 反转链表:双指针法
,指针pre用来表示前驱节点,指针cur用来遍历链表,每次循环改变将pre->cur
的方向改变为pre<-cur
,直到遍历结束。
反转一个单链表。
思路:用一个 prev 节点保存向前指针,temp 保存向后的临时指针
https://leetcode-cn.com/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-by-leetcode-solution-d1k2/
https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484467&idx=1&sn=beb3ae89993b812eeaa6bbdeda63c494&chksm=9bd7fa3baca0732dc3f9ae9202ecaf5c925b4048514eeca6ac81bc340930a82fc62bb67681fa&scene=21#wechat_redirect
方法一:迭代
复杂度分析
时间复杂度:O(n),其中 n 是链表的长度。需要遍历链表一次。
空间复杂度:O(1)。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = nullptr;
ListNode* cur = head;
while (cur) {
ListNode* tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
};
方法二:递归
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (!head || !head->next) {
return head;
}
ListNode* newHead = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return newHead;
}
};
复杂度分析
时间复杂度:O(n),其中 n 是链表的长度。需要对链表的每个节点进行反转操作。
空间复杂度:O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用的栈空间,最多为 n 层。
92. 反转链表 II:双指针法
,指针pre指针指向m的前驱节点,用来将cur的next节点插入到pre后面,指针cur指向位置m起始节点,该节点保持不变,每次需要将cur连接上nxt后边的部分。换句话说,我们要将[m+1,n]的节点每次都要插到位置m之前,这样就完成了反转。
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
思路:先遍历到 m 处,翻转,再拼接后续,注意指针处理
# recurse 递归
# Reverse 反转,颠倒
回溯法(Backtrcking)
读音huí sù,也叫试探法,它是一种系统地搜索问题的解的方法,是一种思想。
有一类问题,我们不知道它明确的计算法则。而是先进行试探,试探到最终状况,发现不满足问题的要求,则回溯到上一个状态继续试探。这种不断试探和回溯的思想,称为回溯法(Backtrcking)
这类问题有求最优解、一组解、求全部解这类问题,例如八皇后问题
【回溯的算法思想】一直往下走,然后再一步步往回走
面对一个问题,每一步有多种做法。先做其中一个做法,然后再此基础上一直做下去,把这个做法的所有可能全部做完,再回来,做第二种做法。
【例子】
深度优先搜索
求一个序列的幂集
八皇后问题
涂色问题
【回溯法实质】它的求解过程实质上是先序遍历一棵“状态树”的过程。
只不过,这棵树不是遍历前预先建立的,而是隐含在遍历过程中。
如果认识到这点,很多问题的递归过程设计也就迎刃而解了。
https://blog.csdn.net/summer_dew/article/details/83921581
解法:
https://leetcode-cn.com/problems/reverse-linked-list-ii/solution/fan-zhuan-lian-biao-ii-by-leetcode/
/**
* Definition for singly-linked list.
* 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) {}
* };
*/
class Solution {
public:
//翻转前N个链表
ListNode* successor=nullptr;
ListNode* reverseN(ListNode* head,int n) {
if (n==1) {
successor=head->next;
return head;
}
ListNode* last= reverseN(head->next,n-1);
head->next->next = head;
head->next =successor;
return last;
}
//反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
ListNode* reverseBetween(ListNode* head, int left, int right) {
if (left==1)
{
return reverseN(head,right);
}
head->next=reverseBetween(head->next,left-1,right-1);
return head;
}
};
方法二: 迭代链接反转:
未通过编译
复杂度分析
时间复杂度: O(N)。考虑包含 N 个结点的链表。对每个节点最多会处理(第 n个结点之后的结点不处理)。
空间复杂度: O(1)。我们仅仅在原有链表的基础上调整了一些指针,只使用了 O(1)的额外存储空间来获得结果。
/**
* Definition for singly-linked list.
* 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) {}
* };
*/
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int m, int n) {
if(!head) return NULL;
ListNode* dummy=new ListNode(-1);
dummy->next=head;
ListNode* a=dummy;
ListNode* d=head;
while(m>1)
{
a=d;
d=d->next;
m=m-1;
n=n-1;
}
ListNode* b=a;
ListNode* c=d;
while (n)
{
ListNode* third=d->next;
d->next=a;
a=d;
d=third;
n=n-1;
}
if(b)
{
b->next = a;
}
else
{
head = a;
c->next=d;
}
return dummy->next;
}
};
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484597&idx=1&sn=c603f1752e33cb2701e371d84254aee2&chksm=9bd7fabdaca073abd512d8fff18016c9092ede45fed65c307852c65a2026d8568ee294563c78&scene=21#wechat_redirect
/**
* Definition for singly-linked list.
* 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) {}
* };
*/
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
if (head == NULL) return NULL;
ListNode *a = head;
ListNode *b = head;
for (int i = 0; i < k; i++) {
if (b == NULL) return head;
b = b->next;
}
ListNode *newNode = reverse(a,b);
a->next = reverseKGroup(b,k);
return newNode;
}
ListNode* reverse(ListNode* a,ListNode *b) {
ListNode *pre, *cur, *third;
pre = NULL; cur = a; third = a;
while (cur != b) {
third = cur->next;
cur->next = pre;
pre = cur;
cur = third;
}
return pre;
}
};
21. 合并两个有序链表:模拟题
,每次循环比较l1->val
和l2->val
,若l1->val
,则在cur后面添加l1
;否则在cur后面添加l2
。
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:通过 dummy node 链表,连接各个元素
//递归
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (l1 == nullptr) {
return l2;
} else if (l2 == nullptr) {
return l1;
} else if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
};
//迭代:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* preHead = new ListNode(-1);
ListNode* prev = preHead;
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
prev->next = l1;
l1 = l1->next;
} else {
prev->next = l2;
l2 = l2->next;
}
prev = prev->next;
}
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev->next = l1 == nullptr ? l2 : l1;
return preHead->next;
}
};
86. 分隔链表:双指针法
,
before_head链表存放比x小的节点,after_head链表存放比x大于或等于的节点,我们分别用before和after来前面两个链表添加节点,用head来遍历原始链表。当原始链表遍历完成时,我们需要将before_head链表连接上after_head链表,即before->next=after_head->next;after->next=nullptr;。
给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
思路:将大于 x 的节点,放到另外一个链表,最后连接这两个链表
哑巴节点使用场景
当头节点不确定的时候,使用哑巴节点
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
ListNode* small = new ListNode(0);
ListNode* smallHead = small;
ListNode* large = new ListNode(0);
ListNode* largeHead = large;
while (head != nullptr) {
if (head->val < x) {
small->next = head;
small = small->next;
} else {
large->next = head;
large = large->next;
}
head = head->next;
}
large->next = nullptr;
small->next = largeHead->next;
return smallHead->next;
}
};
148. 排序链表:归并排序
,先2个2个的 merge,完成一趟后,再 4个4个的 merge,直到结束。
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
思路:归并排序,找中点和合并操作
/*自顶向下归并排序
复杂度分析
时间复杂度:O(n \log n)O(nlogn),其中 nn 是链表的长度。
空间复杂度:O(\log n)O(logn),其中 nn 是链表的长度。空间复杂度主要取决于递归调用的栈空间。
*/
class Solution {
public:
ListNode* sortList(ListNode* head) {
return sortList(head, nullptr);
}
ListNode* sortList(ListNode* head, ListNode* tail) {
if (head == nullptr) {
return head;
}
if (head->next == tail) {
head->next = nullptr;
return head;
}
ListNode* slow = head, *fast = head;
while (fast != tail) {
slow = slow->next;
fast = fast->next;
if (fast != tail) {
fast = fast->next;
}
}
ListNode* mid = slow;
return merge(sortList(head, mid), sortList(mid, tail));
}
ListNode* merge(ListNode* head1, ListNode* head2) {
ListNode* dummyHead = new ListNode(0);
ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;
while (temp1 != nullptr && temp2 != nullptr) {
if (temp1->val <= temp2->val) {
temp->next = temp1;
temp1 = temp1->next;
} else {
temp->next = temp2;
temp2 = temp2->next;
}
temp = temp->next;
}
if (temp1 != nullptr) {
temp->next = temp1;
} else if (temp2 != nullptr) {
temp->next = temp2;
}
return dummyHead->next;
}
};
/*
方法二:自底向上归并排序
复杂度分析
时间复杂度:O(n \log n)O(nlogn),其中 nn 是链表的长度。
空间复杂度:O(1)O(1)。
*/
class Solution {
public:
ListNode* sortList(ListNode* head) {
if (head == nullptr) {
return head;
}
int length = 0;
ListNode* node = head;
while (node != nullptr) {
length++;
node = node->next;
}
ListNode* dummyHead = new ListNode(0, head);
for (int subLength = 1; subLength < length; subLength <<= 1) {
ListNode* prev = dummyHead, *curr = dummyHead->next;
while (curr != nullptr) {
ListNode* head1 = curr;
for (int i = 1; i < subLength && curr->next != nullptr; i++) {
curr = curr->next;
}
ListNode* head2 = curr->next;
curr->next = nullptr;
curr = head2;
for (int i = 1; i < subLength && curr != nullptr && curr->next != nullptr; i++) {
curr = curr->next;
}
ListNode* next = nullptr;
if (curr != nullptr) {
next = curr->next;
curr->next = nullptr;
}
ListNode* merged = merge(head1, head2);
prev->next = merged;
while (prev->next != nullptr) {
prev = prev->next;
}
curr = next;
}
}
return dummyHead->next;
}
ListNode* merge(ListNode* head1, ListNode* head2) {
ListNode* dummyHead = new ListNode(0);
ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;
while (temp1 != nullptr && temp2 != nullptr) {
if (temp1->val <= temp2->val) {
temp->next = temp1;
temp1 = temp1->next;
} else {
temp->next = temp2;
temp2 = temp2->next;
}
temp = temp->next;
}
if (temp1 != nullptr) {
temp->next = temp1;
} else if (temp2 != nullptr) {
temp->next = temp2;
}
return dummyHead->next;
}
};
143. 重排链表:首尾指针法
,首先将原始链表的每一个节点存放在一个数组中,然后我们取首尾指针向中间遍历,每次循环我们需要将左指针的节点连上右指针的节点,在节点连上之后,我们需要将右指针连上未排序的首节点。
给定一个单链表 L:L→L→…→L__n→L 将其重新排列后变为: L→L__n→L→L__n→L→L__n→…
思路:找到中点断开,翻转后面部分,然后合并前后两个链表
class Solution {
public:
void reorderList(ListNode* head) {
if (head == nullptr) {
return;
}
ListNode* mid = middleNode(head);
ListNode* l1 = head;
ListNode* l2 = mid->next;
mid->next = nullptr;
l2 = reverseList(l2);
mergeList(l1, l2);
}
ListNode* middleNode(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* nextTemp = curr->next;
curr->next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
void mergeList(ListNode* l1, ListNode* l2) {
ListNode* l1_tmp;
ListNode* l2_tmp;
while (l1 != nullptr && l2 != nullptr) {
l1_tmp = l1->next;
l2_tmp = l2->next;
l1->next = l2;
l1 = l1_tmp;
l2->next = l1;
l2 = l2_tmp;
}
}
};
141. 环形链表:快慢指针法
,若存在环最终快慢指针会相遇;若不存在环,那么快指针一定会先走到链表尾部。
给定一个链表,判断链表中是否有环。
思路:快慢指针,快慢指针相同则有环,证明:如果有环每走一步快慢指针距离会减 1
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
if (head == nullptr || head->next == nullptr) {
return false;
}
ListNode* slow = head;
ListNode* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if (fast == slow) {
return true;
}
}
return false;
}
};
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
思路:快慢指针,快慢相遇之后,慢指针回到头,快慢指针步调一致一起移动,相遇点即为入环点
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
while (fast != nullptr) {
slow = slow->next;
if (fast->next == nullptr) {
return nullptr;
}
fast = fast->next->next;
if (fast == slow) {
ListNode *ptr = head;
while (ptr != slow) {
ptr = ptr->next;
slow = slow->next;
}
return ptr;
}
}
return nullptr;
}
};
234. 回文链表palindrome-linked-list
234. 回文链表:快慢指针法
,快指针走两步,慢指针走一步,找到链表的中点。然后,翻转后半部分。最后从前半部分链表和后半部分链表是否相同。
请判断一个链表是否为回文链表。
234. 回文链表
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
方法三:我们可以分为以下几个步骤:
找到前半部分链表的尾节点。
反转后半部分链表。
判断是否为回文。
恢复链表。
返回结果。
链接:https://leetcode-cn.com/problems/palindrome-linked-list/solution/hui-wen-lian-biao-by-leetcode/
class Solution {
public:
bool isPalindrome(ListNode* head) {
if (head == nullptr) {
return true;
}
// 找到前半部分链表的尾节点并反转后半部分链表
ListNode* firstHalfEnd = endOfFirstHalf(head);
ListNode* secondHalfStart = reverseList(firstHalfEnd->next);
// 判断是否回文
ListNode* p1 = head;
ListNode* p2 = secondHalfStart;
bool result = true;
while (result && p2 != nullptr) {
if (p1->val != p2->val) {
result = false;
}
p1 = p1->next;
p2 = p2->next;
}
// 还原链表并返回结果
firstHalfEnd->next = reverseList(secondHalfStart);
return result;
}
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* nextTemp = curr->next;
curr->next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
ListNode* endOfFirstHalf(ListNode* head) {
ListNode* fast = head;
ListNode* slow = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
};
中等
138. 复制带随机指针的链表:模拟题
,分三步,第一步在原链表的每个节点后面拷贝出一个新的节点,第二步拷贝random,第三步断开链表。
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。 要求返回这个链表的 深拷贝。
思路:1、hash 表存储指针,2、复制节点跟在原节点后面
https://leetcode-cn.com/problems/fu-za-lian-biao-de-fu-zhi-lcof/solution/jian-zhi-offer-35-fu-za-lian-biao-de-fu-zhi-ha-xi-/
/*
思路一:哈希
借助哈希保存节点信息。
代码
时间复杂度:O(n)
空间复杂度:O(n)
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head == nullptr) return nullptr;
Node* cur = head;
unordered_map map;
// 3. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
while(cur != nullptr) {
map[cur] = new Node(cur->val);
cur = cur->next;
}
cur = head;
// 4. 构建新链表的 next 和 random 指向
while(cur != nullptr) {
map[cur]->next = map[cur->next];
map[cur]->random = map[cur->random];
cur = cur->next;
}
// 5. 返回新链表的头节点
return map[head];
}
};
难度简单107
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
示例 1:
输入:head = [1,3,2]
输出:[2,3,1]
C++4种解法:reverse反转法、堆栈法、递归法、改变链表结构法
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
vector res;
vector reversePrint(ListNode* head) {
//方法1:reverse反转法
/*
while(head){
res.push_back(head->val);
head = head->next;
}
//使用algorithm算法中的reverse反转res
reverse(res.begin(),res.end());
return res;
*/
//方法2:入栈法
/*
stack s;
//入栈
while(head){
s.push(head->val);
head = head->next;
}
//出栈
while(!s.empty()){
res.push_back(s.top());
s.pop();
}
return res;
*/
//方法3:递归
/*
if(head == nullptr)
return res;
reversePrint(head->next);
res.push_back(head->val);
return res;
*/
//方法4:改变链表结构
ListNode *pre = nullptr;
ListNode *next = head;
ListNode *cur = head;
while(cur){
next = cur->next;//保存当前结点的下一个节点
cur->next = pre;//当前结点指向前一个节点,反向改变指针
pre = cur;//更新前一个节点
cur = next;//更新当前结点
}
while(pre){//上一个while循环结束后,pre指向新的链表头
res.push_back(pre->val);
pre = pre->next;
}
return res;
}
};
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
注意:此题对比原题有改动
示例 1:
输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
示例 2:
输入: head = [4,5,1,9], val = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
说明:
free
或 delete
被删除的节点 class Solution {
public:
ListNode* deleteNode(ListNode* head, int val) {
if(head->val == val) return head->next;
ListNode *pre = head, *cur = head->next;
while(cur != nullptr && cur->val != val) {
pre = cur;
cur = cur->next;
}
if(cur != nullptr) pre->next = cur->next;
return head;
}
};
难度简单146
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6
个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6
。这个链表的倒数第 3
个节点是值为 4
的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
/*
快慢指针
定义两个指针,快指针 fastfast, 慢指针 lowlow .
让 fastfast 先向前移动 kk 个位置,然后 lowlow 和 fastfast 再一起向前移动 .
当 fastfast 到达链表尾部,返回 lowlow .
*/
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode* fast = head;
ListNode* low = head;
while (fast != NULL) {
fast = fast->next;
if (k == 0) {
low = low->next;
} else {
k--;
}
}
return low;
}
};
/*
递归
先一直递归到链表尾部,再返回
定义位置变量 pospos ,每次函数返回时 pos++pos++
当 pos == kpos==k 时,说明此时递归函数位于第 kk 个结点位置,返回该结点
*/
class Solution {
public:
int pos = 0;
ListNode* getKthFromEnd(ListNode* head, int k) {
if (head == NULL) {
return 0;
}
ListNode* ret = getKthFromEnd(head->next, k);
pos++;
if (pos == k) {
return head;
}
return ret;
}
};
链表必须要掌握的一些点,通过下面练习题,基本大部分的链表类的题目都是手到擒来~
null/nil 异常处理
dummy node 哑巴节点
快慢指针
插入一个节点到排序链表
从一个链表中移除一个节点
翻转链表
合并两个链表
找到链表的中间节点
remove-duplicates-from-sorted-list
remove-duplicates-from-sorted-list-ii
reverse-linked-list
reverse-linked-list-ii
merge-two-sorted-lists
partition-list
sort-list
reorder-list
linked-list-cycle
linked-list-cycle-ii
palindrome-linked-list
copy-list-with-random-pointer
1. 两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
方法一:暴力枚举
/*
方法一:暴力枚举
复杂度分析
时间复杂度:O(N^2),其中 N 是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。
空间复杂度:O(1).
*/
class Solution {
public:
vector twoSum(vector& nums, int target) {
int n = nums.size();
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (nums[i] + nums[j] == target) {
return {i, j};
}
}
}
return {};
}
};
/*
方法二:哈希表
复杂度分析
时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x。
空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。
*/
class Solution {
public:
vector twoSum(vector& nums, int target) {
unordered_map hashtable;
for (int i = 0; i < nums.size(); ++i) {
auto it = hashtable.find(target - nums[i]);
if (it != hashtable.end()) {
return {it->second, i};
}
hashtable[nums[i]] = i;
}
return {};
}
};
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式
存储的,并且它们的每个节点只能存储一位数字。
如果我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
模拟题,由于链表是逆序存放数字的,所以链表数字从左至右低位对低位,高位对高位,
因此我们从左至右遍历两个链表模拟加法运算即可,注意向高位进位。
这个解法里面的图片很形象
https://leetcode-cn.com/problems/add-two-numbers/solution/hua-jie-suan-fa-2-liang-shu-xiang-jia-by-guanpengc/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode *head = nullptr, *tail = nullptr;
int carry = 0;
while (l1 || l2) {
int n1 = l1 ? l1->val: 0;
int n2 = l2 ? l2->val: 0;
int sum = n1 + n2 + carry;
if (!head) {
head = tail = new ListNode(sum % 10);
} else {
tail->next = new ListNode(sum % 10);
tail = tail->next;
}
carry = sum / 10;
if (l1) {
l1 = l1->next;
}
if (l2) {
l2 = l2->next;
}
}
if (carry > 0) {
tail->next = new ListNode(carry);
}
return head;
}
};
编写一个程序,找到两个单链表相交的起始节点。
方法三:双指针法
创建两个指针 pA 和 pB,分别初始化为链表 A 和 B 的头结点。然后让它们向后逐结点遍历。
当 pA 到达链表的尾部时,将它重定位到链表 B 的头结点 (你没看错,就是链表 B); 类似的,当 pB 到达链表的尾部时,将它重定位到链表 A 的头结点。
若在某一时刻 pA 和 pB 相遇,则 pA/pB 为相交结点。
想弄清楚为什么这样可行, 可以考虑以下两个链表: A={1,3,5,7,9,11} 和 B={2,4,9,11},相交于结点 9。 由于 B.length (=4) < A.length (=6),pB 比 pA 少经过 22 个结点,会先到达尾部。将 pB 重定向到 A 的头结点,pA 重定向到 B 的头结点后,pB 要比 pA 多走 2 个结点。因此,它们会同时到达交点。
如果两个链表存在相交,它们末尾的结点必然相同。因此当 pA/pB 到达链表结尾时,记录下链表 A/B 对应的元素。若最后元素不相同,则两个链表不相交。
复杂度分析
时间复杂度 : O(m+n)。
空间复杂度 : O(1)。
视频牛逼啊https://leetcode-cn.com/problems/intersection-of-two-linked-lists/solution/jiao-ni-yong-lang-man-de-fang-shi-zhao-dao-liang-2/
https://leetcode-cn.com/problems/intersection-of-two-linked-lists/solution/tu-jie-xiang-jiao-lian-biao-by-user7208t/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *A = headA, *B = headB;
while (A != B) {
A = A != nullptr ? A->next : headB;
B = B != nullptr ? B->next : headA;
}
return A;
}
};
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:给定的 n 保证是有效的。
进阶:你能尝试使用一趟扫描实现吗?
快慢指针法
,起始快指针走n步后,若此时快指针已为空,表示我们删除第一个节点,直接返回head->next即可;否则此时快慢指针一起走,也就是慢指针走size-n步到达倒数第N个节点的前驱节点,快指针会到达链表的尾节点,此时我们删除slow->next节点即可。
链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/chao-ke-ai-dong-hua-jiao-ni-ru-he-shan-chu-lian-bi/
/**
* Definition for singly-linked list.
* 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) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0,head);
ListNode* first = dummy;
ListNode* second = dummy;
for (int i = 0; i <= n; ++i) {
first = first->next;
}
while (first) {
first = first->next;
second = second->next;
}
second->next = second->next->next;
return dummy->next;
}
};
模拟题
,直接遍历链表确定是否删除节点即可。
删除链表中等于给定值 val 的所有节点。
示例:
输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5
复杂度分析
/**
* Definition for singly-linked list.
* 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) {}
* };
*/
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummy=new ListNode(0,head);
ListNode* pre=dummy;
ListNode* cur=head;
while(cur)
{
if (cur->val==val)
{
pre->next=cur->next;
}
else
{
pre=cur;
}
cur=cur->next;
}
return dummy->next;
}
};
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
示例 2:
输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
奇数 :Odd number
偶数:even numbers
https://leetcode-cn.com/problems/odd-even-linked-list/solution/zui-po-su-de-xiang-fa-dai-ma-zhu-shi-fei-chang-xia/
/**
* Definition for singly-linked list.
* 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) {}
* };
*/
class Solution {
public:
ListNode* oddEvenList(ListNode* head) {
if (head == nullptr) {
return head;
}
ListNode* evenHead = head->next;
ListNode* odd = head;
ListNode* even = evenHead;
while (even != nullptr && even->next != nullptr) {
odd->next = odd->next->next;
odd = odd->next;
even->next = even->next->next;
even = even->next;
}
odd->next = evenHead;
return head;
}
};
模拟题
,迭代法,遍历链表,若发现该链表存在child节点那么就将[child,tail]这段子链表插入到当前节点的后面去,然后继续遍历链表。
多级双向链表中,除了指向下一个节点和前一个节点指针之外,它还有一个子链表指针,可能指向单独的双向链表。这些子列表也可能会有一个或多个自己的子项,依此类推,生成多级数据结构,如下面的示例所示。
给你位于列表第一级的头节点,请你扁平化列表,使所有结点出现在单级双链表中。
复杂度分析
模拟题
,先求出链表长度size,若k取余size为空,那么不用旋转了,直接返回head;否则将链表首尾相连形成环形链表,由于k表示尾节点移动k%size位,那么头节点移动size-k%size位。
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
示例 1:
输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL
示例 2:
输入: 0->1->2->NULL, k = 4
输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL
向右旋转 2 步: 1->2->0->NULL
向右旋转 3 步: 0->1->2->NULL
向右旋转 4 步: 2->0->1->NULL
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if(!head) return NULL;
int n=0;
for(auto p=head;p;p=p->next) n++;
k%=n;
auto first=head,second=head;
while(k--){
first=first->next;
}
while(first->next){
first=first->next;
second=second->next;
}
first->next=head;
head=second->next;
second->next=NULL;
return head;
}
};