移除链表元素(leetcod.203)
注意点:
- 应该用一个指针维护要删除节点的前驱节点,而不是维护要删除的节点,因为删除链表元素必须知道前驱节点
虚拟头节点
// 虚拟头节点
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
struct ListNode* dummyHead = new ListNode(0,head);
struct ListNode* cur = dummyHead;
while(cur->next != nullptr){
if(cur->next->val == val){
cur->next = cur->next->next;
}else{
cur = cur->next;
}
}
return dummyHead->next;
}
};
常规做法
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 删除头节点
while(head != nullptr && head->val == val){
head = head->next;
}
struct ListNode* cur = head;
while(cur != nullptr && cur->next != nullptr){
if(cur->next->val == val){
cur->next = cur->next->next;
}else{
cur = cur->next;
}
}
return head;
}
};
注意点:
- 不用虚拟头节点,直接从头节点开始遍历,则需要额外考虑删除头节点的情况
设计链表
单链表
class MyLinkedList {
public:
MyLinkedList() {
//虚拟头节点
this->size = 0;
this->head = new ListNode(0);
}
int get(int index) {
// 小于0 或者 为虚拟头节点(等于的情况)/超出大小、
// 比如要求链表下标为0处的节点,由于0被虚拟头节点占了,所以实际上是要求下标为1处的节点,即0=0不合法
if (index < 0 || index >= size) {
return -1;
}
ListNode *cur = head;
// 有虚拟头节点,所以要小于等于(多一次++)
for (int i = 0; i <= index; i++) {
cur = cur->next;
}
return cur->val;
}
void addAtHead(int val) {
addAtIndex(0, val);
}
void addAtTail(int val) {
addAtIndex(size, val);
}
// index=size 插入尾部,所以不需要合法性判断
void addAtIndex(int index, int val) {
if (index > size) {
return;
}
index = max(0, index);
size++;
ListNode *pred = head;
// 遍历到插入位置之前
for (int i = 0; i < index; i++) {
pred = pred->next;
}
ListNode *toAdd = new ListNode(val);
toAdd->next = pred->next;
pred->next = toAdd;
}
// index=size超出范围,没有元素,无法删除
void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
ListNode *pred = head;
for (int i = 0; i < index; i++) {
pred = pred->next;
}
ListNode *p = pred->next;
pred->next = pred->next->next;
delete p;
}
private:
int size;
ListNode *head;
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
注意点:
- 由于使用虚拟头节点,实际上查找下标为n处的节点,对应于size-1处,所以对于查找操作,需要对index=size合法性判断
- 对于增加操作,插入下标为n的节点,如果n=size,正好说明是插到链表末尾
- 对于删除操作,删除下标为n的节点,如果n=size,同查找操作一样,说明不合法
反转链表(leetcode.206)
迭代
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr) {
ListNode* temp = curr->next;
curr->next = prev;
prev = curr;
curr = temp;
}
return prev;
}
};
注意点:
- 反转链表,那么原先的头节点最后成为尾节点,这个尾节点的next需要指向null,因此需要一个prev初始为null,用于反转方向的被指向方
- 从原先头节点开始遍历,依次反转,但由于反转需要修改指向断开原有的链,如果链断开了,原链表就无法继续遍历,所以需要一个temp来记录当前cur节点的下一个节点,从而可以继续遍历
- 遍历过程中cur节点和prev指针都要依次移动,如果先移动cur,那么prev就无法移动到cur移动前的位置,所以需要prev指向cur的节点后cur才能移动
递归
//递归,顾名思义是要到最里一层开始操作,在这里也就是从最后一个节点开始往回操作
class Solution {
public:
ListNode* reverseList(ListNode* head) {
//判空,head为null说明传入的是空链表,head.next为null说明已经找到最后一个节点,直接返回即可
if (!head || !head->next) {
return head;
}
//当到达最后一个节点时,返回的newHead就是最后一个节点
ListNode* newHead = reverseList(head->next);
//而head就是前一个节点,因此将next反转即可
head->next->next = head;
//每次递归执行这一步是为了在最后到达1节点时,它的next可以指向空,否则会出现环
head->next = nullptr;
return newHead;
}
};
两两交换链表中的节点(leetcode.24)
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
struct ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
struct ListNode* cur = dummyHead;
while(cur->next!=nullptr&&cur->next->next!=nullptr){
struct ListNode* temp = cur->next;
struct ListNode* temp1 = cur->next->next->next;
cur->next=cur->next->next;
cur->next->next=temp;
temp->next=temp1;
cur=cur->next->next;
}
return dummyHead->next;
}
};
1->2->3->4->5->null
1->2->3->4->5->6->null
注意点:
-
考虑两种情况:
-
节点数为奇数,最后一个节点不需要交换
-
节点数为偶数,恰好所有节点两两交换
-
-
两两交换链表节点,则头节点开始就得交换,又因为两两交换节点其实就是反转链表的缩小版(长度为2),因此根据一样的思路,我们需要一个 prev / cur 来记录每个交换对中的前驱节点
-
又因为最后需要返回链表的头节点,所以需要一个指针始终保持不动用于记录,可以使用虚拟头节点
-
cur从虚拟头节点出发,每次交换cur后的两个节点,所以判空条件是cur->next和cur->next->next,这两个条件具有顺序性,cur->next对应奇数情况,cur->next->next对于偶数情况,由于奇数必定在偶数之前,所以cur->next&&cur->next->next的顺序不能变
-
由于虚拟头节点的next用于记录要返回的链表节点,则这个next必指向原链表的第二个节点,从指向head改为指向原链表第二个节点,需要断链,则后面交换对的交换则丢失了第一个节点的信息,所以需要一个temp记录交换对中的第一个节点
-
两两交换节点,则交换对中的第二个节点next应指向第一个节点,这样就把链断开了,因此需要一个temp1记录下一个交换对的第一个节点
环形链表(leetcode.142)
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(true){
if(fast == nullptr || fast->next == nullptr)return nullptr;
fast = fast->next->next;
slow = slow->next;
if(fast == slow){
break;//第一次相遇(n = 1)
//f = 2s 且 f = s + nb
}
}
//寻找环的入口,对于所有刚好走到环入口的指针,都走了
//k = a + nb步,其中a为链表头部到链表入口的节点个数
//(不计链表入口节点),b为环的周长,
//现在相遇时slow指针走了nb步,只需要求出a的值即可
fast = head;//相遇后就让fast重新从head出发
while(slow != fast){
slow = slow->next;
fast = fast->next;
}
return fast;
}
};
注意点:
- 快指针与慢指针相对速度差1,所以不存在未相遇前,快指针跳过慢指针的情况
- 慢指针在环里还没走完一圈一定会与快指针相遇(也就是n一定等于1)
- 相遇后根据算式可知,此时从链表头节点到环形入口处的长度 与 从相遇点到环形入口处的长度相同,因此重置速度为1后,fast从head出发 与 slow从相遇处出发,再次相遇时一定是在环形入口处
合并两个升序列表(leetcode.21)
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* preHead = new ListNode(-1);
ListNode* prev = preHead;
while(list1 != nullptr && list2 != nullptr){
if(list1->val < list2->val){
prev->next = list1;
list1 = list1->next;
}else{
prev->next = list2;
list2 = list2->next;
}
prev = prev->next;
}
prev->next = list1 == nullptr ? list2 : list1;
return preHead->next;
}
};
注意点:
- 最后要返回一个新的链表,所以需要有一个虚拟头节点记录头节点
- 合并实际上是在新链表的某位一直插入节点,所以需要一个prev来遍历
- 哪个子链表先遍历到null,则新链表的末尾直接连上另一个子链表即可
旋转链表(leetcode.61)
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if(head == nullptr || head->next == nullptr || k == 0) return head;
// 保存结果
ListNode* ans = head;
int n = 0;
ListNode* curr = head;
// 获取链表长度
while(curr != nullptr){
curr = curr->next;
n++;
}
// 计算右移几次
int m = 0;
if( n > k){
m = k;
}else if(n < k){
m = k % n;
}else{
return ans;
}
// 开始移动
int i = 1;
while( i <= m){
// 移动一次
ans = rightMove(ans);
i++;
}
return ans;
}
private:
ListNode* rightMove(ListNode* head){
ListNode* curr = head;
// 找到倒数第二个节点
while(curr->next->next != nullptr){
curr = curr->next;
}
// 获取最后一个节点
ListNode* end = curr->next;
// 砍断它
curr->next = nullptr;
// 粘到头上
end->next = head;
// 返回新的头头
return end;
}
};
注意点:
- 由于要返回的链表的头节点是在旋转过程中时刻发生变化的,所以就不需要虚拟头节点来记录链表的头节点了
删除排序链表中的重复元素(leetcode.83)
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if(!head){
return head;
}
ListNode* cur = head;
while(cur->next){
if(cur->val == cur->next->val){
ListNode* rec = cur->next;
cur->next = cur->next->next;
delete(rec);
}else{
cur = cur->next;
}
}
return head;
}
};
注意点:
- 因为重复元素最后会保留一个,所以不用担心头节点被删除,即不需要另外加一个指针维护头节点
- 由于是排序好的,所以可以直接遍历,判断前后两个节点是否重复
- 询问面试官是否要释放节点
删除排序链表中的重复元素Ⅱ(leetcode.82)
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (!head) {
return head;
}
ListNode* dummy = new ListNode(0, head);
ListNode* cur = dummy;
while (cur->next && cur->next->next) {
if (cur->next->val == cur->next->next->val) {
int x = cur->next->val;
while (cur->next && cur->next->val == x) {
cur->next = cur->next->next;
}
}
else {
cur = cur->next;
}
}
return dummy->next;
}
};
注意点:
- 由于重复元素全部删除,所以头节点可能被删除,需要另外加一个指针(虚拟头节点)来不断维护新的头节点
- 由于cur从虚拟头节点开始,所以是判断next与next->next是否相同,如果相同,先记录值,然后逐步删除next直到next的val不为记录值