目录

链表理论基础

203. 移除链表元素

常规解法

虚拟头节点解法

递归解法

707. 设计链表

单链表解法

双链表解法

206. 反转链表

递归解法

双指针解法


链表理论基础

链表和数组一样,都是能存储多个数据的结构。

它们的区别是:数组存的元素在内存中是排列在一起的,链表存的元素在内存中的位置是随机的,一个元素除了有数据外,还有一个指向下一个元素的指针。

所以,对数组进行查询操作的时间复杂度是O(1),因为计算机已经知道某个下标的元素是放在哪,而查询链表元素的时间复杂度是O(n),在最极端的情况下,我们需要从链表的第一个元素查询到最后一个元素才找到我们需要的值。

那链表是不是就不如数组了?非也非也,链表在增加、删除元素的时候比数组高效。比如我们想在数组中删除某个位置的元素,就必须把其后的元素一个个地前移,时间复杂度O(n);而对于链表,我们找到那个位置的元素后,只需把它前一个位置的元素的指针修改为指向它后一个位置,以O(1)的时间复杂度就能完成删除元素操作。

除了前述提到的单向链表外,还有双向链表(其中元素存取上一个节点和下一个节点的指针,可以双向查询)、循环链表(尾节点指向头节点,也可以说没有头节点和尾巴节点,像一条咬住自己尾巴的蛇一样,可以用来解决约瑟夫环问题)。

值得一提的是,在C++中我们需要对没用的内存位置进行内存释放,不然爆堆就糟糕了。

文章链接:https://programmercarl.com/%E9%93%BE%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html

203. 移除链表元素

常规解法

要删除(单向)链表的某个元素,必须在那个元素的前一个元素(cur)的位置来删,即cur->next = cur->next->next。对于头节点,它没有前一个元素,就需要分类讨论了。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        while (head != nullptr && head->val == val) {
            ListNode* tmp = head;
            head = head->next;
            delete tmp;
        }

        ListNode* cur = head;
        while(cur != nullptr && cur->next != nullptr) {
            if(cur->next->val == val) {
                ListNode *tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            }
            else {
                cur = cur->next;
            }
        }
        return head;
    }
};

虚拟头节点解法

一种省事的方法是,在头节点的前边再加一个节点,这个节点我们称之为虚拟头节点,那么头节点就可以和其他节点一样遍历而不需要分类讨论了。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead = new ListNode();
        dummyHead->next = head;
        ListNode* cur = dummyHead;
        while(cur != nullptr && cur->next != nullptr) {
            if (cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            }
            else {
                cur = cur->next;
            }
        }
        head = dummyHead->next;
        delete dummyHead;
        return head;
    }
};

递归解法

时空复杂度都是O(n),在项目中不太建议用这种解法,链表长了可能会栈溢出,但可以练练递归的思维。

通过观察removeElements的输入输出参数格式,容易想到当头结点head元素值为val时,只需 removeElements(head->next, val),即输入参数改成下一个元素值,就可以起到删除此时节点的作用。

那为啥我们不直接:

ListNode* head = removeElements(head->next, val);
return head;

还要创个新变量:

ListNode* newHead = removeElements(head->next, val);
delete head;
return newHead;

会不会像脱裤子放屁多此一举呢?

不,这个做法很重要,可以防止内存泄漏(memory leak)

你删除的head对应的内存,也是Leetcode帮你new出来的。new出来的内存空间就存在堆(heap)中,空间有限,如果你不用它,就要用delete把它删了,不然用完了就凉了。

当头节点元素不为val时,只需head->next = removeElements(head->next, val),最后return head。在此判断内不会修改head的值,只会做删除元素的操作,即进入“传入removeElements的第一个参数值不为val时”的判断,或者继续往下遍历。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if (head == nullptr) return nullptr;
        if (head->val == val) {
            ListNode* newHead = removeElements(head->next, val);
            delete head;
            return newHead;
        }
        else {
            head->next = removeElements(head->next, val);
            return head;
        }
    }
};

707. 设计链表

单链表解法

使用虚拟头节点,简化了增删操作。

需要注意的点:

1、增加节点的时候,先newNode->next = cur->next再cur->next = newNode(脑子里可以想一下对应的图);

2. 删除节点的时候,先cur->next = cur->next->next,把cur->next架空了,再把cur->next对应的那块内存放到ListNode* tmp里,delete掉后还需给tmp赋值nullptr,防止错误访问到个野指针。

3. addAtIndex函数的题目要求是“如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。”,注意不合法范围和get不一样。

class MyLinkedList {
public:
    struct LinkNode {
        int val;
        LinkNode* next;
        LinkNode(int val) : val(val), next(nullptr){}
    };

    MyLinkedList () {
        _dummyHead = new LinkNode(0);
        _size = 0;
    }

    int get(int index) {
        if(index < 0 || index > _size - 1) return -1;
        LinkNode* cur = _dummyHead->next;
        while(index--) {
            cur = cur->next;
        }
        return cur->val;
    }
    
    void addAtHead(int val) {;
        LinkNode* newNode = new LinkNode(val);
        newNode->next = _dummyHead->next;
        _dummyHead->next = newNode;
        _size++;
    }
    
    void addAtTail(int val) {
        LinkNode* newNode = new LinkNode(val);
        LinkNode* cur = _dummyHead;
        while(cur->next != nullptr)
            cur = cur->next;
        cur->next = newNode;
        _size++;
    }
    
    void addAtIndex(int index, int val) {
        if(index < 0 || index > _size) return;
        LinkNode* newNode = new LinkNode(val);
        LinkNode* cur = _dummyHead;
        while(index--) {
            cur = cur->next;
        }
        newNode->next = cur->next;
        cur->next = newNode;
        _size++;
    }
    
    void deleteAtIndex(int index) {
        if(index < 0 || index > _size - 1) return;
        LinkNode* cur = _dummyHead;
        while(index--) {
            cur = cur->next;
        }
        LinkNode* tmp = cur->next;
        cur->next = cur->next->next;
        delete tmp;
        tmp = nullptr;
        _size--;
    }

private:
    int _size;
    LinkNode* _dummyHead;
};

/**
 * 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);
 */

双链表解法

先占个坑。

206. 反转链表

递归解法

其实和双指针解法的逻辑是一样的。

有一个链表,使用pre和cur进行遍历,在遍历的过程中反转(改next的指向)。cur一开始指向head,pre一开始指向nullptr(对应原链表尾节点指向的nullptr)。设计递归函数ListNode* reverse(ListNode* pre, ListNode* cur),当cur为空(遍历到原链表的最末尾元素)返回pre(新链表的头指针);当链表不为空,记录值为cur->next的tmp,作为下一轮的cur,再做链表反转的操作,把cur->next指向pre,之后开启下一轮的reverse(cur, tmp)。在主函数中,直接return reverse(nullptr, head),作为反转的开始。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverse(ListNode* pre, ListNode* cur) {
        if (cur != nullptr) {
            ListNode* tmp = cur->next;
            cur->next = pre;
            return reverse(cur, tmp);
        }
        else return pre;
    }
    ListNode* reverseList(ListNode* head) {
            return reverse(nullptr, head);
        }
};

双指针解法

和递归解法逻辑是一样的,可能更好写。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        while(cur != nullptr) {
            ListNode* tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;    
    }
};

posted on 2026-01-19 23:19  快乐的乙炔  阅读(1)  评论(0)    收藏  举报  来源