算法----链表

112 阅读8分钟

移除链表元素(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不为记录值