链表的基础知识
链表的结构
节点
- 数据域
- 指针域
链状结构
通过指针域的值形成了一个线性结构
访问链表的时间复杂度
链表不适合快速的定位数据,适合动态的插入和删除数据的应用场景。
- 查找节点 O(n)
- 插入节点 O(1)
- 删除节点 O(1)
几种经典的链表实现方法
- 传统方法 (节点 + 指针)
- 使用数组模拟
1)指针域与数据域分离
2)利用数组存放下表进行索引
链表的典型应用场景
1.操作系统内的动态内存分配
2.LRU缓存淘汰算法
经典面试题
链表的访问
- 思路
1)方法一:采用额外的哈希表,遍历链表,如果哈希表中出现了遍历的元素,则认为有环,如果直到NULL,哈希表中都没出现重复元素,则无环。
2)方法二:利用快慢指针,慢指针一次走一步,快指针一次走两步,如果在到达尾部NULL之前,快指针追上了慢指针,则有环,否则,则无环。 - 代码
bool hasCycle(ListNode *head) {
if (head == NULL) {
return false;
}
ListNode *p = head, *q = head->next;
while (p != q && q && q->next) {
p = p->next;
q = q->next->next;
}
return q && q->next;
}
- 思路
利用快慢指针,慢指针走一步,快指针走两步,找到两个指针的相遇点,无相遇则返回无环的NULL,有相遇的情况下,将慢指针置于起点位置,然后快慢指针每次走一步,再次相遇的点就是入环点。 - 代码
//代码1
ListNode *detectCycle(ListNode *head) {
if (head == NULL) return NULL;
ListNode *p = head, *q = head;
while (q && q->next) {
p = p->next;
q = q->next->next;
if (p == q) {
p = head;
while (p != q) {
p = p->next;
q = q->next;
}
return p;
}
}
return NULL;
}
//代码二
ListNode *detectCycle(ListNode *head) {
if (head == NULL) return NULL;
ListNode *p = head, *q = head;
if (q->next == NULL) return NULL;
do {
p = p->next;
q = q->next->next;
} while(p != q && q && q->next);
if (q == NULL || q->next == NULL) return NULL;
p = head;
while(p != q) p = p->next, q = q->next;
return q;
}
注意: 代码一和代码二思路一样,但是代码二的更好,没有循环中套循环,更优雅。
- 思路
转换为判断链表是否有环的问题。 - 代码
int getNext(int x) {
int z = 0;
while (x) {
z += (x % 10) * (x % 10);
x /= 10;
}
return z;
}
bool isHappy(int n) {
int p = n , q = n;
do {
p = getNext(p);
q = getNext(getNext(q));
} while (p != q && q != 1);
return q == 1;
}
链表的反转
- 思路
1)迭代反转
2)递归反转 - 代码
/*迭代反转*/
ListNode* reverseList(ListNode* head) {
if (head == nullptr) return nullptr;
ListNode *pre = NULL, *cur = head, *p = head->next;
while (cur) {
cur->next = pre;
pre = cur;
(cur = p) && (p = p->next);
}
return pre;
}
/*递归反转*/
ListNode* reverseList(ListNode* head) {
if (head == nullptr || head->next == nullptr) return head;
ListNode *tail = head->next, *p = reverseList(head->next);
head->next = tail->next;
tail->next = head;
return p;
}
- 思路
使用虚拟头节点,因为链表的头有可能改变。 - 代码
/*反转链表的前n个节点*/
ListNode* reverseN(ListNode* head, int n) {
if (n == 1) { return head; }
ListNode *tail = head->next, *p = reverseN(head->next, n - 1);
head->next = tail->next;
tail->next = head;
return p;
}
ListNode* reverseBetween(ListNode* head, int left, int right) {
if (head == nullptr) {
return nullptr;
}
ListNode ret(0, head), *p = &ret;
int m = left;
//找到反转片段的前一个节点
while (--m) p = p->next;
p->next = reverseN(p->next, right - left + 1);
return ret.next;
}
- 思路
先判断是否有K个节点,然后对这K个节点翻转,最后拼接。依然需要虚拟头节点。 - 代码
/*翻转K个链表,不足K个时返回头*/
ListNode* reverseN(ListNode* head, int K) {
if (K == 1) {return head;}
int m = K;
ListNode* cur = head;
while (--m && cur) cur = cur->next;
//不足K个时返回头
if (cur == nullptr) return head;
ListNode* tail = head->next, *p = reverseN(head->next, K - 1);
head->next = tail->next;
tail->next = head;
return p;
}
ListNode* reverseKGroup(ListNode* head, int k) {
if (head == nullptr) return head;
ListNode ret(0, head), *p = &ret, *q = head;
//如果p->next == q 说明没有翻转成功,跳出循环
//不等于则代表翻转成功,此时q就是下一组K节点的前一个节点
while ((p->next = reverseN(p->next, k)) != q) {
p = q;
q = q->next;
}
return ret.next;
}
//优化reverseN
ListNode* __reverseN(ListNode* head, int K) {
if (K == 1) {return head;}
ListNode* tail = head->next, *p = reverseN(head->next, K - 1);
head->next = tail->next;
tail->next = head;
return p;
}
ListNode* reverseN(ListNode* head, int K) {
int m = K;
ListNode* cur = head;
while (--m && cur) cur = cur->next;
if (cur == nullptr) return head;
return __reverseN(head, K);
}
- 思路
1)求链表的长度 n, 但是要同时找到最后一个节点
2)将尾节点指向头节点
3)由于是向右旋转, 则从头节点向右移动 n - k - 1 步就是旋转后的新链表尾节点, 尾节点后一个即是新链表头节点, 断开并返回新头即可 - 代码
ListNode* rotateRight(ListNode* head, int k) {
if (head == nullptr) return head;
//求链表的长度 n, 但是要同时找到最后一个节点
int n = 1;
ListNode* cur = head;
while (cur->next) n++, cur = cur->next;
//将尾节点指向头节点
cur->next = head;
//由于是向右旋转, 则从头节点向右移动 n - k - 1 步就是旋转后的新链表尾节点, 尾节点后一个即是新链表头节点, 断开并返回新头即可.
k = n - k % n;
//此行可以不加,但是 下面的while循环要改成 k--
cur = head;
while (--k) cur = cur->next;
//此时 cur 即是 旋转后链表的 尾
ListNode* newhead = cur->next;
cur->next = NULL;
return newhead;
}
- 思路
LeetCode #25 题的一个特例,K = 2 时的一个特例. - 代码
ListNode* __reverseN(ListNode* head, int K) {
if (K == 1) {return head;}
ListNode* tail = head->next, *p = reverseN(head->next, K - 1);
head->next = tail->next;
tail->next = head;
return p;
}
ListNode* reverseN(ListNode* head, int K) {
int m = K;
ListNode* cur = head;
while (--m && cur) cur = cur->next;
if (cur == nullptr) return head;
return __reverseN(head, K);
}
ListNode* reverseKGroup(ListNode* head, int k) {
if (head == nullptr) return head;
ListNode ret(0, head), *p = &ret, *q = head;
while ((p->next = reverseN(p->next, k)) != q) {
p = q;
q = q->next;
}
return ret.next;
}
ListNode* swapPairs(ListNode* head) {
return reverseKGroup(head, 2);
}
链表的节点删除
- 思路
利用虚拟头节点和快慢指针。找到删除节点的前一个节点,执行删除即可。 - 代码
ListNode* removeNthFromEnd(ListNode* head, int n) {
if (head == nullptr) return nullptr;
ListNode ret(0, head), *p = &ret, *q = head;
while (n-- && q) q = q->next;
while (q != nullptr) {
p = p->next;
q = q->next;
}
//此时p就是要删除节点的前一个节点
p->next = p->next->next;
return ret.next;
}
- 思路
见代码,很简单,迭代即可。 - 代码
ListNode* deleteDuplicates(ListNode* head) {
if (head == nullptr) return head;
ListNode* p = head;
while (p->next) {
if (p->val == p->next->val) {
p->next = p->next->next;
} else {
p = p->next;
}
}
return head;
}
三. LeetCode #82 删除排序链表中的重复节点II.
- 思路
利用虚拟头节点,因为新链表的头节点有可能发生变化。 - 代码
ListNode* deleteDuplicates(ListNode* head) {
if (head == nullptr) {
return nullptr;
}
ListNode ret(0, head), *p = &ret, *q;
while (p->next) {
if (p->next->next && p->next->next->val == p->next->val) {
int val = p->next->val;
q = p->next->next;
while (q && val == q->val) {
q = q->next;
}
p->next = q;
} else {
p = p->next;
}
}
return ret.next;
}