写在前面的提示
- 通过一些测试用例可以节省您的时间
使用链表时不易调试。因此,在编写代码之前,自己尝试几个不同的示例来验证您的算法总是很有用的。 - 你可以同时使用多个指针
有时,当你为链表问题设计算法时,可能需要同时跟踪多个结点。您应该记住需要跟踪哪些结点,并且可以自由地使用几个不同的结点指针来同时跟踪这些结点。如果你使用多个指针,最好为它们指定适当的名称,以防将来必须调试或检查代码。 - 在许多情况下,你需要跟踪当前结点的前一个结点
你无法追溯单链表中的前一个结点。因此,您不仅要存储当前结点,还要存储前一个结点。
练习
1. 反转链表
题目链接: 206.反转链表
解题思路:
- 迭代,时间复杂度O(n),空间复杂度O(1)
- 递归,时间复杂度O(n),空间复杂度O(n),由于使用递归,将会使用隐式栈空间。递归深度可能会达到 n 层
- 双指针,时间复杂度O(n),空间复杂度O(1)
建议食用题解中某大佬的思路展示,图文结合非常清晰here
代码:
//迭代版本,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == NULL || head->next == NULL) return head;
ListNode *newhead = head;
while(head->next != NULL){
ListNode *p = head->next;
head->next = p->next;
p->next = newhead;
newhead = p;
}
return newhead;
}
};
//递归版本,时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == NULL || head->next == NULL) return head;
ListNode *newhead = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return newhead;
}
};
2. 移除链表元素
题目链接: 203. 移除链表元素
解题思路:题目较简单,dummyhead的使用使得讨论的case大大减少。
- 迭代结合虚拟头节点,注意循环中当next需要被删除时,当前遍历到的r指针不能前进,否则会丢失处理对象。时间复杂度O(n),空间复杂度O(1)
- 递归实现,假设当前节点后面都已经处理好,转而判断当前节点val是否为目标val,进而返回对应结果
代码:
//递归版本
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(head == NULL) return head;
ListNode *ret = removeElements(head->next, val);
if(head->val == val) return ret;
head->next = ret;
return head;
}
};
//迭代+虚拟头节点
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(head == NULL) return head;
ListNode *dummyhead = new ListNode(0);
dummyhead->next = head;
ListNode* r = dummyhead;
while(r->next != NULL){
if(r->next->val == val){
r->next = r->next->next;
}else{//此时若下一个节点需要删除,那么r应该保留位置,而不是前进
r = r->next;
}
}
return dummyhead->next;
}
};
3. 奇偶链表
题目链接: 328. 奇偶链表
解题思路:
- 奇偶链表先拆分,后合并,思路简单清晰,到末尾的处理很关键,涉及while循环的终止条件,自己编程的结果明显想的复杂,虽通过,但不如题解中给出的简洁明了。
代码:
class Solution {
public:
ListNode* oddEvenList(ListNode* head) {
if(head == NULL || head->next == NULL || head->next->next == NULL) return head;
ListNode *oddhead = head, *oddp = head;
ListNode *evenhead = head->next, *evenp = head->next;
while(oddp->next != NULL && evenp->next != NULL){
if(oddp->next != NULL && oddp->next->next != NULL){
oddp->next = oddp->next->next;
oddp = oddp->next;
}
else if(oddp->next == NULL || oddp->next->next == NULL) oddp->next = NULL;
if(evenp->next != NULL && evenp->next->next != NULL){
evenp->next = evenp->next->next;
evenp = evenp->next;
}
else if(evenp->next == NULL || evenp->next->next == NULL) evenp->next = NULL;
}
oddp->next = evenhead;
return oddhead;
}
};
//官方题解
class Solution {
public:
ListNode* oddEvenList(ListNode* head) {
if(head == NULL || head->next == NULL || head->next->next == NULL) return head;
ListNode *oddhead = head, *oddp = head;
ListNode *evenhead = head->next, *evenp = head->next;
while(evenp != NULL && evenp->next != NULL){
oddp->next = evenp->next;
oddp = oddp->next;
evenp->next = oddp->next;
evenp = evenp->next;
}
oddp->next = evenhead;
return oddhead;
}
};
4. 回文链表
题目链接: 234. 回文链表
解题思路:说几个比较推荐的解题思路
- 构造数组判断,由于链表的特性对元素的访问十分消耗时间,通过构建对应的数组,可以进一步使用双指针从两端逐一比对。缺点是空间复杂度达到了O(n)
- 在原数组上利用快慢指针,将链表一分为二,反转其中的一部分,再与另一部分进行比较,若完全相同则是回文链表,利用该方法可以实现O(1)的额外空间复杂度,另外要注意应恢复链表的结构,由于此题对此不要求,我的代码中没有恢复链表的结构。该题中关于快慢指针和反转的实现细节,题解中的大神提出了不同的方案:
a. 传统方案,快慢指针和反转分为独立的两个部分,通过慢指针确定反转的位置, 这里反转的是后半部分的链表,然后再进行反转操作,最后与前半部分链表比较。
b. 加鸡腿方案, 快慢指针的遍历和反转操作融合在一起,使用一个循环即可, 这里反转的是链表的前半部分, 当慢指针遍历的同时对前半部分进行反转操作, 慢指针遍历结束后的位置可确定后半部分链表的开始位置, 再比较即可. - 哈希公式, 似乎利用公式对回文的处理非常快,码住会了回来补充!
- 暴力递归+暴力迭代 (逐对定位比较,很蠢)
代码:
//快慢指针反转后半部分链表进行比较,
class Solution {
public:
bool isPalindrome(ListNode* head) {
if(head == NULL || head->next == NULL) return true;
ListNode *slow = head, *fast = head;
while(fast != NULL && fast->next != NULL && fast->next->next != NULL){
fast = fast->next->next;
slow =slow->next;
}
ListNode *cur = slow->next, *pre = NULL;
ListNode *save = cur->next;
while(save != NULL){
cur->next = pre;
pre = cur;
cur = save;
save = save->next;
}
cur->next = pre;
ListNode *p = cur;
ListNode *q = head;
while(p != NULL){
if(q->val != p->val) return false;
p = p->next;
q = q->next;
}
return true;
}
};
//快慢指针遍历翻转一遍过,
class Solution {
public:
bool isPalindrome(ListNode* head) {
if(head == NULL || head->next == NULL) return true;
if(head->next->next == NULL) return head->val == head->next->val;
ListNode *slow = head, *fast = head;
ListNode *pre = NULL, *save = head->next;
while(fast != NULL && fast->next != NULL && fast->next->next != NULL){
fast = fast->next->next;
slow->next = pre;
pre = slow;
slow = save;
save = save->next;
}
slow->next = pre;
if(fast->next == NULL) slow = slow->next;
while(save != NULL){
if(save->val != slow->val) return false;
save = save->next;
slow = slow->next;
}
return true;
}
};
5. 合并两个有序链表
题目链接: 21. 合并两个有序链表
解题思路:
经典迭代的想法,两个指针从两个链表各自出发分别比较,每次迭代中符合条件的插入到新链表后面,时间复杂度O(m+n),空间复杂度O(1)
代码:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode *p1 = l1, *p2 = l2;
ListNode* resdummyhead = new ListNode(-1);
ListNode* restail = resdummyhead;
while(p1 != NULL && p2 != NULL){
if(p1->val < p2->val){
restail->next = p1;
restail = p1;
p1 = p1->next;
}
else{
restail->next = p2;
restail = p2;
p2 = p2->next;
}
}
if(p1 == NULL) restail->next = p2;
if(p2 == NULL) restail->next = p1;
return resdummyhead->next;
}
};
6. 两数相加
题目链接: 2. 两数相加
解题思路:
- 考虑到两个链表的低位在表头,所以可以依次从头开始对节点进行相加,同时注意用变量存储进位的情况,在两者都遍历完之后,若仍有进位,应该在结尾再补充一个节点。时间/空间复杂度O(max(m,n))
- 我尝试过用数值直接进行计算再分解的操作,这对于一些长度较小的链表是可行的,但是当树木变大,传统的int,long等类型无法存储那么大的数会导致算法崩溃。
代码:
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode *p1 = l1;
ListNode *p2 = l2;
ListNode *resdummyhead = new ListNode(-1);
ListNode *resp = resdummyhead;
int count = 0;
while(p1 != nullptr || p2 != nullptr){
int addres;
if(p1 == nullptr) addres = p2->val + count;
else if(p2 == nullptr) addres = p1->val + count;
else addres = p1->val + p2->val + count;
count = 0;
if(addres >= 10){
count = 1;
addres = addres - 10;
}
ListNode *r = new ListNode(addres);
resp->next = r;
resp = r;
if(p1) p1 = p1->next;
if(p2) p2 = p2->next;
}
if(count == 1){
ListNode *r = new ListNode(1);
resp->next = r;
}
p1 = resdummyhead->next;
delete resdummyhead;
return p1;
}
};
7. 扁平化多级双向链表
题目链接: 430. 扁平化多级双向链表
解题思路:
- 递归解决,很容易想到,每次检测到某节点child不为null,假设指向的链表已经flaten,然后进行操作。由于用了递归其消耗的空间较大
- 利用while循环迭代解决,代码清晰,思路非常巧妙,能够成功的原因是每次p处理完了相应的child链表后,往前进的时候其实是会先遍历完原来的子链表,这样实际上就完成了一个深度遍历,推荐!
代码:
//递归版本
class Solution {
public:
Node* flatten(Node* head) {
if(head == NULL) return NULL;
Node *p = head;
while(p != NULL){
if(p->child != NULL){
Node *temp = flatten(p->child);
Node *r = temp;
while(r->next != NULL) r = r->next;
p->child = NULL;
temp->prev = p;
r->next = p->next;
p->next = temp;
if(r->next != NULL)
r->next->prev = r;
p = r->next;
}
else p = p->next;
}
return head;
}
};
//迭代版本,推荐!
class Solution {
public:
Node* flatten(Node* head) {
Node *p = head;
while(p != NULL){
if(p->child != NULL){
Node *child = p->child;
Node *next = p->next;
p->child =NULL;
child->prev = p;
p->next = child;
while(child->next != NULL) child = child->next;
child->next = next;
if(next != NULL)
next->prev = child;
}
p = p->next;
}
return head;
}
};
8.
题目链接: 138. 复制带随机指针的链表
解题思路:
- 遍历原链表复制每个节点在对应原节点的右边(next指针指向),生成一个2*N的链表,遍历的时候对于random不为空的节点可以依靠相对位置确定其右边节点的random指针指向的位置。此方法只用到O(1)的额外空间复杂度。
- 递归迭代解放见官方题解,第一种思路是其中第三种解法,前两种方法难懂且最后额外的空间复杂度为O(n)。
代码:
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head == NULL) return NULL;
Node *p = head;
while(p != NULL){
Node *node = new Node(p->val);
node->next = p->next;
p->next = node;
p = p->next->next;
}
p = head;
while(p != NULL){
if(p->random != NULL) p->next->random = p->random->next;
p = p->next->next;
}
p = head;
Node *newhead = head->next;
Node *np = newhead;
while(p != NULL){
p->next = np->next;
p = p->next;
if(p != NULL){
np->next = p->next;
np = np->next;
}
}
return newhead;
}
};
9. 旋转链表
题目链接: 61. 旋转链表
解题思路:
- 注意到旋转次数到达链表长度的时候i相当于没有旋转,因此实际旋转的效果=k%length的结果,可以将原链表的尾部next链接到原链表head,由k与length计算出应该切断的地方,找到即可。时间复杂度O(n),空间复杂度O(1)
代码:
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if(head == nullptr || head->next == nullptr) return head;
int length = 1;
ListNode *p = head;
while(p->next != nullptr){
p = p->next;
length++;
}
p->next = head;
k = k % length;
p = head;
for(int i = 0; i < length-k-1; ++i){
p = p->next;
}
head = p->next;
p->next = nullptr;
return head;
}
};