文章目录
以下为Datawhale Leetcode开源学习思路总结,以下代码均为Leetcode代码,但不一定是最优解,仅供参考学习。
链表基础
707. 设计链表
本题主要练习链表的增删查,在设计这个类时,应考虑代码是否可以复用,addAtHead
、addAtTail
可以用addAtIndex
表示,只要实现了addAtIndex
,那么addAtHead
和addAtTail
就只是其中的一个子类,那么明确以后我们开始设计,代码如下:
struct Node {
Node(int value, Node* next = nullptr) : value(value), next(next) {}
int value;
Node* next;
};
class MyLinkedList {
public:
MyLinkedList() = default;
MyLinkedList(const MyLinkedList&) = delete;
MyLinkedList(MyLinkedList&&) = delete;
int get(int index) {
Node* cur = head;
if (index >= size || index < 0) return -1;
while (index--)
cur = cur->next;
return cur->value;
}
void addAtHead(int val) {
addAtIndex(0, val);
}
void addAtTail(int val) {
addAtIndex(size, val);
}
void addAtIndex(int index, int val) {
if (index > size || index < 0) return;
Node** pcur = &head;
while (index--)
pcur = &((*pcur)->next);
Node* new_node = new Node(val, *pcur);
*pcur = new_node;
size++;
}
void deleteAtIndex(int index) {
if (index >= size || index < 0) return;
Node** pcur = &head;
while (index--)
pcur = &((*pcur)->next);
Node* tmp = *pcur;
*pcur = (*pcur)->next;
delete tmp;
size--;
}
~MyLinkedList() {
while (head) {
Node* tmp = head;
head = head->next;
delete tmp;
}
}
private:
Node* head = nullptr;
int size = 0;
};
我记着我当时做的时候,遇到比较蠢的问题是在遍历链表操作时直接对原来的head
指针进行操作了,而非使用一个临时的指针去遍历,还是太粗心了。
206. 反转链表
这道题需要三个指针,首先是left
、right
指针用于修改指向,还需要一个tmp
指针保存right
的下一个节点的位置。
主要逻辑代码:
while(right) {
tmp = right->next;
right->next = left;
left = right;
right = tmp;
}
然后进行遍历最终当链表翻转过来以后,把末尾至null
,也就是head->next = nullptr
,如图:
此时left
就是最终的首节点,返回即可。具体代码如下:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == nullptr || head->next == nullptr) return head;
ListNode *left = head;
ListNode *right = left->next;
ListNode *tmp = nullptr;
while(m) {
tmp = right->next;
right->next = left;
left = right;
right = tmp;
}
head->next = nullptr;
return left;
}
};
203. 移除链表元素
我们知道要进行链表的删除,那必然需要知道删除节点(cur
指向)和删除节点前一位节点(pre
指向),还需要记录删除节点的下一位(tmp
指向),我们可以构造一个头节点,这样构造之后,对链表的操作就会简单很多,删除节点的操作大致如下:
pre->next = cur->next; // 删除操作
tmp = cur->next; // 保存cur下一位
delete cur; // 释放删除掉的节点的空间
cur = tmp; // cur恢复为下一位
具体细节就不一一赘述了,代码如下:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(head == nullptr) return head;
ListNode *begin = new ListNode(0);
begin->next = head;
ListNode *l = begin;
ListNode *r = head;
int flag = 0;
ListNode *tmp = nullptr;
while(r) {
if(r->val == val) {
flag = 1;
l->next = r->next;
tmp = l->next;
delete r;
r = tmp;
}
if(flag == 0) {
l = r;
r = r->next;
}
flag = 0;
}
head = begin->next;
delete begin;
begin = nullptr;
return head;
}
};
328. 奇偶链表
这道题其实非常巧妙,两个指针错位连接,将一个链表分为两个链表,奇数位串起来,偶数位串起来,根据链表总节点数为奇数还是偶数,最后在拼接链表时会稍有不同,可以分为两种情况去处理,具体代码如下:
class Solution {
public:
ListNode* oddEvenList(ListNode* head) {
if(head == nullptr || head->next == nullptr || head->next->next == nullptr) return head;
//至少三个元素
ListNode *l = head;
ListNode *r = l->next;
ListNode *tmp = r;
ListNode *loc_l = l;
ListNode *loc_r = r;
while(r) {
loc_l = l->next;
loc_r = r->next;
l->next = r->next;
l = loc_l;
r = loc_r;
}
l = head;
while(l->next) {
l = l->next;
}
l->next = tmp;
// l->next = tmp;
return head;
}
};
234. 回文链表
本题我的思路是,找中间位置,然后将链表一分为二,左半部分入栈,然后出栈的同时,链表右边部分进行遍历,与出栈元素进行比较,如果均相同,说明是回文数。这个思路解题比较简单。代码如下:
class Solution {
public:
bool isPalindrome(ListNode* head) {
if(head == nullptr || head->next == nullptr) return true;
ListNode *tmp = head;
int cnt = 0;
int flag = 0; // 0 表示偶数 1表示奇数
while(tmp) {
cnt++;
tmp = tmp->next;
}
if(cnt % 2 == 0) flag = 0; //判断奇数还是偶数
else flag = 1;
cnt /= 2;
stack<int> s;
tmp = head;
for(int i = 0; i < cnt; ++i) {
s.push(tmp->val);
tmp = tmp->next;
}
if(flag == 1) tmp = tmp->next;
for(int i = 0; i < cnt; ++i) {
if(s.top() == tmp->val) {
s.pop();
tmp = tmp->next;
continue;
}
return false;
}
return true;
}
};
138. 复制带随机指针的链表
这道题我之前讲过,视频链接
主要思路是:首先建立map
,key
为源节点地址,value
为源节点index
,的null
在map
的key
表示为"null"
,value
只需要给最后一个真实节点的index
加1
即可,把它作为null
的value
。我们再建立一个vector
用于存放下标索引到真实索引的指向,遍历vector
,从而在构建新链表时初始化random
的指向。具体可参考视频链接,这里直接贴出本思路的代码实现:
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head == nullptr) return nullptr;
Node *res_node = new Node(0);
Node *firstptr = head;
Node *go = res_node;
unordered_map<Node *, int> mp;
vector<int> node_vec;
int id = 0;
while(firstptr) {
Node *tmp = new Node(firstptr->val);
mp[firstptr] = id++;
go->next = tmp;
go = go->next;
firstptr = firstptr->next;
}
mp[nullptr] = id++;
firstptr = head;
while(firstptr) {
node_vec.push_back(mp[firstptr->random]);
firstptr = firstptr->next;
}
go = res_node->next;
int i = 0;
while(go) {
firstptr = res_node->next;
int index = node_vec[i];
while(index--) {
firstptr = firstptr->next;
}
go->random = firstptr;
++i;
go = go->next;
}
return res_node->next;
}
};
链表排序
148. 排序链表
本题首先最简单的就是直接将值写入数组,对数组排序后恢复链表。另一种方法则是归并,我们需要找中间值,将一个链划分为两部分,一直递归到最深处,即每个状态都分为左状态和右状态,接着就是对左右两个无法继续递归的状态比较大小然后进行合并,代码中sortList
用于递归传入左右两个状态的起始指针,merge
则用于对左右状态进行合并。
具体代码如下:
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(head == nullptr || head->next == nullptr) return head;
//至少两个节点
ListNode *slow = head;
ListNode *fast = head->next;
/* 不论链的奇偶 slow走至左链末尾 fast走至右链末尾 */
while(fast != nullptr && fast->next != nullptr) {
slow = slow->next; // slow每次走一步
fast = fast->next->next; // fast每次走两步
}
fast = slow->next;
slow->next = nullptr;
return merge(sortList(head), sortList(fast));
}
ListNode *merge(ListNode *l1, ListNode *l2) {
ListNode dump(0); // 只是辅助头节点
ListNode *cur = &dump;
while(l1 != nullptr && l2 != nullptr) {
if(l1->val < l2->val) {
cur->next = l1;
l1 = l1->next;
} else {
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;
}
if(l1 != nullptr)
cur->next = l1;
else
cur->next = l2;
return dump.next;
}
};
21. 合并两个有序链表
本题比较简单,两个链表一起遍历,最终需要注意肯定有一个链表先遍历结束,那么后结束的只需要加入到之前合并的链当中即可,代码如下:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode *head = new ListNode();
ListNode *tmp = head;
while(list1 && list2) {
if(list1->val <= list2->val) {
tmp->next = list1;
list1 = list1->next;
} else {
tmp->next = list2;
list2 = list2->next;
}
tmp = tmp->next;
}
if(list1 == nullptr)
tmp->next = list2;
else
tmp->next = list1;
return head->next;
}
};
147. 对链表进行插入排序
要进行插入排序,必然要涉及到对链表节点的删除和添加,删除节点需要知道要删除节点的位置和前一个位置;添加节点需要知道当前添加位置的前一个位置,我们使用begin
去指向一个头节点,方便后续对链表的操作,代码如下:
class Solution {
public:
ListNode* insertionSortList(ListNode* head) {
if(head == nullptr) return head;
ListNode *begin = new ListNode(0);
begin->next = head;
ListNode *cur = begin->next;
ListNode *l = begin;
ListNode *tmp = nullptr;
int max_num = head->val;
while(cur) {
if(cur->val < max_num) {
l->next = cur->next;
tmp = cur;
cur = tmp->next;
// 将tmp插入到合适位置
ListNode *left = begin;
ListNode *right = begin->next;
while(right->val < tmp->val) {
left = left->next;
right = right->next;
}
// 插入
tmp->next = left->next;
left->next = tmp;
} else {
max_num = cur->val;
cur = cur->next;
l = l->next;
}
}
return begin->next;
}
};
链表双指针
141. 环形链表I / 142. 环形链表II
这两个题类似,我们直接按照142
去讲,141
呢是直接判断链表是否可以成环,142
则是判断是否可以成环,如果成环,第一个成环的地址返回。那么以142
为例,这题有两种思路:
- 第一种方法使用
unordered_set
,遍历链表,将每个链表节点的地址加入到set
,如果set
中已存在,则第一个发现已存在的地址则是我要需要返回的地址,若遍历结束还未找到,则返回nullptr
,说明未找到。 - 第二种方法是通过快慢双指针,快指针步长为
2
,慢指针步长为1
,所以如果有环,最终快慢指针一定相遇,但是有个问题就是相遇以后的位置并不一定是成环的第一个位置,那么如下图有一个数学推导:
按照快慢双指针的方法,最终head
到第一个成环地址的距离a
等于快慢指针meet
以后到第一个成环地址的距离,即上图中的a = c
,因此我们只需要从head
和meet
开始遍历,当两者相遇时,返回的就是成环的第一个地址。
最后,我分别列出141
和142
的代码:
141:
class Solution {
public:
bool hasCycle(ListNode *head) {
// 思路1 使用set求环起始节点
#if 0
unordered_set<ListNode *> s;
ListNode *tmp = head;
while(tmp) {
if(s.find(tmp) != s.end()) {
return true;
}
s.insert(tmp);
tmp = tmp->next;
}
return false;
#endif
// 思路2 快慢指针
ListNode *fast = head;
ListNode *slow = head;
ListNode *meet = nullptr;
while(fast) {
slow = slow->next;
fast = fast->next;
if(!fast) {
return false;
}
fast = fast->next;
if(fast == slow) {
return true;
}
}
return false;
}
};
142:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
// 思路1 使用set求环起始节点
#if 0
unordered_set<ListNode *> s;
ListNode *tmp = head;
// int flag = 1;
while(tmp) {
if(s.find(tmp) != s.end()) {
return tmp;
}
s.insert(tmp);
tmp = tmp->next;
}
return nullptr;
#endif
// 思路2 快慢指针
ListNode *fast = head;
ListNode *slow = head;
ListNode *meet = nullptr;
while(fast) {
slow = slow->next;
fast = fast->next;
if(!fast) {
return nullptr;
}
fast = fast->next;
if(fast == slow) {
meet = fast;
break;
}
}
if(meet == nullptr) {
return nullptr;
}
while(head && meet) {
if(head == meet) {
return head;
}
head = head->next;
meet = meet->next;
}
return nullptr;
}
};
19. 删除链表的倒数第N个结点
这道题的思路是,两个指针left
和right
,控制间距为n
(n
是题目中给出的倒数第几个数),然后以相同速度遍历链表,当right
不能遍历时,left
的下一位则是要删除的倒数第N
个节点。代码如下:
ListNode *removeNthFromEnd(ListNode *head, int n)
{
if (!head)
return nullptr;
ListNode new_head(-1);
new_head.next = head;
ListNode *slow = &new_head, *fast = &new_head;
for (int i = 0; i < n; i++)
fast = fast->next;
while (fast->next) {
fast = fast->next;
slow = slow->next;
}
ListNode *to_de_deleted = slow->next;
slow->next = slow->next->next;
delete to_be_deleted;
return new_head.next;
}