- 小技巧
- 链表的基本操作
- 206. 反转链表">206. 反转链表
- 237. 删除链表中的节点(神思维)">237. 删除链表中的节点(神思维)
- 83. 删除排序链表中的重复元素">83. 删除排序链表中的重复元素
- 21. 合并两个有序链表(使用dummy)">21. 合并两个有序链表(使用dummy)
- 24. 两两交换链表中的节点(记得画图,常看)">24. 两两交换链表中的节点(记得画图,常看)
- 其他链表操作
- 160. 相交链表(两个指针)">160. 相交链表(两个指针)
- 234. 回文链表(链表中点+反转链表)">234. 回文链表(链表中点+反转链表)
- 328. 奇偶链表">328. 奇偶链表
- 19. 删除链表的倒数第 N 个结点(快慢指针)">19. 删除链表的倒数第 N 个结点(快慢指针)
- 148. 排序链表(归并排序)">148. 排序链表(归并排序)
- 147. 对链表进行插入排序">147. 对链表进行插入排序
小技巧
由于在进行链表操作时,尤其是删除节点时,经常会因为对当前节点进行操作而导致内存或指针出现问题。有两个小技巧可以解决这个问题:
- 一是尽量处理当前节点的下一个节点而非当前节点本身
- 二是建立一个虚拟节点,使其指向当前链表的头节点,这样即使原链表所有的节点全部删除,也会有一个虚拟节点存在,返回 dummy->next即可。
-
链表的基本操作
206. 反转链表
迭代
关键在于一个pre指针,以及一个next指针
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr,*next;
while(head != nullptr){
next = head -> next;
head -> next = prev;
prev = head;//prev表示前一个节点
head = next;//将头节点进行更新
}
return prev;
}
};
递归(从后往前)
在递归中,当前节点是操作不了前面的节点的,所以cur->next = pre是不可能的,
- 只能使用cur->next->next= cur,让后面的节点指向当前的节点。
这里从后往前,先进入后面,然后再回归的时候进行操作。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (!head || !head->next) {//如果head为空或者,head->next为空返回。
return head;
}
ListNode* newHead = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return newHead;
}
};
递归(从前往后)
和迭代一样,从前往后操作,先操作然后进入。需要一个dummy节点。来实现先存储cur->next,然后cur->next = pre,
//只要找到最后一个节点全部都返回。把最后一个节点返回。
class Solution {
public:
ListNode* reverseList(ListNode* head, ListNode* dummy = nullptr) {
if(!head) {
return dummy;
}
ListNode* node = head->next;
head->next = dummy;
return reverseList(node, head);//只要找到最后一个节点全部都返回。把最后一个节点返回。
}
};
237. 删除链表中的节点(神思维)
因为链表不知道前面的一个节点是无法完成中间节点的删除的。这里有一个神思维,直接把要删除的节点变成下一个节点,然后把原本不需要删除的下一个节点删除掉。
class Solution {
public:
void deleteNode(ListNode* node) {
node->val=node->next->val;
node->next=node->next->next;
return ;
}
};
引发思考
这里为什么不用下面的代码来删除节点呢?
class Solution {
public:
void deleteNode(ListNode* node) {
node = node->next;
}
};
因为这里的node本质上你可以看作是一个装地址的容器,你改变它的值本质上只是改变了这个容器的值,并没有改变原本链表的结构,只能通过node->next来进入到一个节点里面来改变结构。
83. 删除排序链表中的重复元素
- 需要一个dummy来记住头节点
注意delete养成好习惯,回收堆空间,防止内存泄漏。
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
ListNode *dummy = head;
while(dummy&&dummy->next){
if(dummy->val==dummy->next->val) {
ListNode *next = dummy->next;
dummy->next = next->next;
delete(next);
} else {
dummy = dummy->next;
}
}
return head;
}
};
21. 合并两个有序链表(使用dummy)
迭代
朴实无华的思路,和归并排序里面的排序有点像
class Solution {
public:
ListNode* l3;
ListNode* mergeTwoLists(ListNode *l1, ListNode *l2) {
ListNode *dummy = new ListNode(0), *node = dummy;//这里就用到了那个思想,需要一个前置节点dummy
while (l1 && l2) {
if (l1->val <= l2->val) {
node->next = l1;
l1 = l1->next;
} else {
node->next = l2;
l2 = l2->next;
}
node = node->next;
}
node->next = l1? l1: l2;
return dummy->next;
}
};
递归
思路想岔了,不应该新建一个节点
- 因为新建的节点很难传递到递归的下一层。
ListNode* head1 = new ListNode();//如果没有后面的就是申请了一个空指针
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* head1();
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
dfs(list1, list2, head1);
return head1->next;
}
void dfs(ListNode* list1, ListNode* list2, ListNode* head){
if(!list1){
head->next = list2;
return ;
}
if(!list2){
head->next = list1;
return ;
}
if(list1->val<list2->val){
head->next = list1;
dfs(list1->next, list2, head->next);
} else {
head->next = list2;
dfs(list1, list2->next, head->next);
}
}
};
24. 两两交换链表中的节点(记得画图,常看)
迭代写法
- 需要一个dummy
画图就会清晰很多
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* temp = dummyHead;
while (temp->next != nullptr && temp->next->next != nullptr) {
ListNode* node1 = temp->next;
ListNode* node2 = temp->next->next;
temp->next = node2;
node1->next = node2->next;
node2->next = node1;
temp = node1;
}
return dummyHead->next;
}
};
递归
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode* newHead = head->next;
head->next = swapPairs(newHead->next);
newHead->next = head;
return newHead;
}
};
其他链表操作
160. 相交链表(两个指针)
从自己的头开始效率会低,从对方的头开始效率会高
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *l1 = headA,*l2 = headB;
while(l1!=l2){
l1 = l1?l1->next:headB;
l2 = l2?l2->next:headA;
}
return l1;
}
};
234. 回文链表(链表中点+反转链表)
先得到链表的中点,然后将后半部分反转
同时遍历,看是否一样
class Solution {
public:
bool isPalindrome(ListNode* head) {
int num = 0;
ListNode *mid = head;
while(mid){
mid = mid->next;
num++;
}//得到最大长度
num = num%2==0? num/2:num/2+1;//取中位数
mid = head;
for(int i = 0; i < num; i++){
mid = mid->next;
}
mid = reverse(mid);//在中间反转链表
while(head&&mid){
if(head->val!=mid->val)//从中间开始遍历,不同则为false
return false;
head = head->next;
mid = mid->next;
}
return true;
}
ListNode* reverse(ListNode * head){//反转链表
ListNode* pre = nullptr;
ListNode* next;
while(head){
next = head->next;
head->next = pre;
pre = head;
head = next;
}
return pre;
}
};
328. 奇偶链表
对于原始链表,每个节点都是奇数节点或偶数节点。头节点是奇数节点,头节点的后一个节点是偶数节点,相邻节点的奇偶性不同。因此可以将奇数节点和偶数节点分离成奇数链表和偶数链表,然后将偶数链表连接在奇数链表之后,合并后的链表即为结果链表。
even1指的是偶链表的尾节点,odd指的是奇数链表的偶节点。
class Solution {
public:
ListNode* oddEvenList(ListNode* head) {
if(!head) return head;
ListNode* odd = head, * even = head->next;
ListNode* even1 = even;
while(even1&&even1->next){
odd->next = even1->next;
odd = odd->next;
even1->next = odd->next;
even1 = even1->next;
}
odd->next = even;
return head;
}
};
19. 删除链表的倒数第 N 个结点(快慢指针)
使用一个快指针先走到n的地方
- 然后使用一个慢指针从头开始,这样的话,当快指针到达链表尾部的时候,慢指针就到了倒数第n个地方,因为快指针和慢指针之间差n个。
注意删除的是头节点的情况
class Solution { public: ListNode* removeNthFromEnd(ListNode* head, int n) { ListNode* dummy = new ListNode(0), *pre;//使用一个dummy来判断链表是头节点的情况。 ListNode* fast = dummy, *low = dummy; dummy->next = head; int num = 0; while(num<n){ num++; fast = fast->next; } while(fast){ fast=fast->next; pre = low; low = low->next; } pre->next = low->next; pre = dummy->next; delete(dummy);//注意防止内存泄漏的情况 return pre; } };
148. 排序链表(归并排序)
归并排序基于分治算法。最容易想到的实现方式是自顶向下的递归实现,考虑到递归调用的栈空间,自顶向下归并排序的空间复杂度是 )O(logn)。如果要达到 O(1) 的空间复杂度,则需要使用自底向上的实现方式。
自顶向下
对链表自顶向下归并排序的过程如下。
找到链表的中点,以中点为分界,将链 表拆分成两个子链表。寻找链表的中点可以使用快慢指针的做法,快指针每次移动 2 步,慢指针每次移动 11步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点。
- 对两个子链表分别排序。
将两个排序后的子链表合并,得到完整的排序后的链表。可以使用「21. 合并两个有序链表」的做法,将两个有序的子链表进行合并。
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;//为每个小子集加上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; } };
自底向上
用 subLength 表示每次需要排序的子链表的长度,初始subLength=1。
- 每次将链表拆分成若干个长度为 subLength 的子链表(最后一个子链表的长度可以小于 subLength),按照每两个子链表一组进行合并,合并后即可得到若干个长度为 subLength×2 的有序子链表(最后一个子链表的长度可以小于 subLength×2)。合并两个子链表仍然使用「21. 合并两个有序链表」的做法。
将subLength 的值加倍,重复第 2 步,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于 length,整个链表排序完毕。
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; } };
147. 对链表进行插入排序
首先判断给定的链表是不是只有一个
- 创建哑节点dummy,为了方便在头部插入,令dummy->next=head。
- 维护last代表链表已经完成排序的最后一个节点,初始时last=head;
- 维护cur代表last节点的后一个节点,就是即将判断的节点,cur=head->next;
- 比较last节点与cur节点的值
- 如果last节点的值小于cur节点的值,就代表不需要进行别的操作。
- 如果last节点的值大于cur节点的的值,就需要从链表头部dummy开始比较下一个节点的值和cur节点的值的大戏小。prev的下一个节点是第一个比cur大的节点,prev初始为dummy,为什么这里是prev的下一个节点呢,是为了方便后续的cur节点的插入。因为链表难以知道前一个节点的指针。
class Solution { public: ListNode* insertionSortList(ListNode* head) { if(!head->next) return head; ListNode* last = head, *dummy = new ListNode(0); dummy->next = head; ListNode* cur = head->next; while(cur) { if(last->val <= cur->val) { last = last->next; } else { ListNode* first = dummy; while(first->next->val <= cur->val) { first = first->next; } last->next = cur->next; cur->next = first->next; first->next = cur; } cur = last->next; } last = dummy->next; delete(dummy); return last; } };