链表
1. 分类
-
单链表
-
循环链表
-
双向链表
-
双向循环链表
2. 链表 VS 数组性能
时间复杂度 | 数组 | 链表 |
---|---|---|
插入、删除 | O(n) | O(1) |
随机访问 | O(1) | O(n) |
数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,这也是它与数组最大的区别。
3. 链表应用技巧
-
检查边界条件:
-
如果链表为空时,代码是否能正常工作?
-
如果链表只包含一个结点时,代码是否能正常工作?
-
如果链表只包含两个结点时,代码是否能正常工作?
-
代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
-
-
用哨兵节点简化代码
-
我觉得最好用的:画图
4. Leetcode练习题
LeetCode练习题对应编号:206,141,21,19,876。
4.1 206.反转链表
链表题都可以考虑能不能用双指针,这题用两个指针,把链表的指向改变即可:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* cur = nullptr;
ListNode* pre = head;
while(pre != nullptr) {
ListNode* t = pre->next;
pre->next = cur;
cur = pre;
pre = t;
}
return cur;
}
};
4.2 141.环形链表
也是双指针的思路,fast和slow两个指针如果相遇则说明链表中有环:
class Solution {
public:
bool hasCycle(ListNode *head) {
//用两个指针来指示,如果有环则会快慢指针相遇
ListNode* fast = head;
ListNode* slow = head;
while(fast != nullptr) {
fast = fast->next;
if(fast != nullptr) fast = fast->next;
if(slow == fast) return true;
slow = slow->next;
}
return false;
}
};
4.3 21.合并两个有序链表
题目链接:21. 合并两个有序链表 - 力扣(LeetCode)
这题的递归法很妙:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
//递归的做法,非常巧妙
if(list1 == nullptr) {
return list2;
}
if(list2 == nullptr) {
return list1;
}
if(list1->val > list2->val) {
list2->next = mergeTwoLists(list1,list2->next);
return list2;
}
else {
list1->next = mergeTwoLists(list2,list1->next);
return list1;
}
}
};
迭代法用到了哨兵节点来简化代码,哨兵节点可以看成是一个傀儡,作用是防止首节点为空时(first == null),出现无法指向下个节点情况。哨兵节点指向的下个单元为首节点。
迭代法的代码如下:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* result = new ListNode(-1); //设置哨兵节点简化代码
ListNode* rPtr = result; //指针指向result
while(list1 != nullptr && list2 != nullptr) {
if(list1->val < list2->val) {
rPtr->next = list1;
list1 = list1->next;
}
else {
rPtr->next = list2;
list2 = list2->next;
}
rPtr =rPtr->next; //移动指针
}
rPtr->next = list1 != nullptr ? list1:list2; //移动最后剩下的
return result->next; //如果不是指向result->next,返回的结果将有-1
}
};
4.4 19.删除链表的倒数第 N 个结点
题目链接:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
用双指针思路,利用fast和slow之间的差值来找到倒数第n个节点并进行删除:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if(head->next == nullptr) return nullptr; //只有一个数直接返回
ListNode* result = new ListNode(); //设置哨兵节点
result->next = head;
ListNode* slow = result, * fast = result;
while(n >= 0) { //此时fast指针移动n+1次
fast = fast->next;
--n;
}
while(fast != nullptr) { //fast指针总共移动N次,slow指针移动N-n-1次,也就是停在了倒数第n个数的前一位
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next; //slow指针停在n倒数第n+1的数那里,实现删除
return result->next; //哨兵节点要返回它的next,也就是头节点
}
};
分析:fast指针先移动 n+1 次,然后两个指针一起移动,fast指针一共移动 N 次(N是链表的长度),那么slow指针比fast指针少移动 N-n-1 次,也就是在倒数第 n+1 个节点处,在要删除的节点的前面一位,接着直接改变指针的指向即可实现删除。
4.5 876.链表的中间结点
题目链接:876. 链表的中间结点 - 力扣(LeetCode)
同样的双指针思路,让fast比slow多移动 N/2 次即可:
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* fast, * slow = head;
//注意偶数时的边界条件
while(fast != nullptr && fast->next != nullptr) {
fast = fast->next;
if(fast != nullptr) fast = fast->next;
slow = slow->next;
}
return slow;
}
};