代码随想录算法训练营第三天|Leetcode 203.移除链表元素、 707.设计链表 、206.反转链表

文章介绍了如何在LeetCode中处理链表问题,包括删除等于给定值的节点、使用虚拟头节点简化操作、实现链表的基本功能如获取节点值、添加和删除节点,以及单链表和双链表的反转方法。
摘要由CSDN通过智能技术生成

Leetcode 203.移除链表元素

题目链接:Leetcode 203.移除链表元素

题目描述:删除链表中等于给定值 val 的所有节点。

思路:

1.将删除元素的上一个元素指向删除元素的下一个元素

2.释放内存          

​​​​​​注:该图片节选与《代码随想录》

不过因为单链表的特殊性,只能指向下一个节点,刚刚删除的是链表的中第二个,和第四个节点,那么如果删除的是头结点又该怎么办呢?

这里就涉及如下链表操作的两种方式:

  • 直接使用原来的链表来进行删除操作。
  • 设置一个虚拟头结点在进行删除操作。

先看第一种情况:

由于原来的链表头节点为第一个元素,所以删除该元素只需head=head->next,之后释放该节点即可。

第一种方式代码:(不带虚拟头节点)

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        //删除头节点
        while (head != nullptr && head->val == val) {
            ListNode* temp = head;
            head = head->next;
            delete temp;
        }
        //删除非头结点
        ListNode* cur = head;
        while (cur != nullptr && cur->next != nullptr) {
            if (cur->next->val == val) {
                ListNode* temp = cur->next;
                cur->next = cur->next->next;
                delete temp;
            } else {
                cur = cur->next;
            }
        }
        return head;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

由于头节点的删除和其他节点的删除操作有所不同,因此需要判断。那可不可以将操作统一呢?

答案是可以的,既然头节点和其他节点不同点在于头节点之前没有节点,那是不是可以创建一个虚拟头节点放到头节点之前,这样原链表的所有节点就都可以按照统一的方式进行移除了。

第二种方式代码:(带虚拟头节点)

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead = new ListNode(0); //建立虚拟头节点
        dummyHead->next = head;
        ListNode* cur = dummyHead;
        while (cur->next != NULL) {
            if (cur->next->val == val) //如果找到删除结点的前一个结点
            {
                ListNode* temp = cur->next; //保存被删除的节点
                cur->next = cur->next->next;
                delete temp;
            } else //如果没找到
            {
                cur = cur->next;
            }
        }
        head = dummyHead->next; //虚拟头节点的next即原链表的头节点
        delete dummyHead;
        return head;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

Leetcode 707.设计链表

题目链接:Leetcode 707.设计链表

题目描述:

在链表中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val  的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

这道题算是练习单链表和双链表最好的一道题了,包含了链表的三种添加方式,获取某个节点元素的值,删除某个节点。有人可能会说,没有修改操作怎么能算好题呢?实则不然,修改元素是不是只需要:(1)查找到某个节点(2)修改该节点元素的值

(1)这道题已经包括了,(2)就是一行赋值操作。没必要新增一个函数了。

思路:推荐看这个视频,讲的很好:帮你把链表操作学个通透!

我分别用单链表和双链表实现了以上操作,其中双链表很易错!

单链表:

class MyLinkedList {
public:
    //定义链表结构体
    struct LinkList {
        int val;
        LinkList* next;
        LinkList(int val) : val(val), next(nullptr) {}
    };
    //初始化链表
    MyLinkedList() {
        _dummyhead = new LinkList(0);
        _size = 0;
    }
    //获取第index个节点数据
    int get(int index) {
        //如果下标无效则返回-1
        //先判断index是否大于_size-,再判断index是否小于
        if (index > (_size - 1) || index < 0) {
            return -1;
        }
        LinkList* cur = _dummyhead->next;
        while (index--) {
            cur = cur->next;
        }
        return cur->val;
    }
    //头插法
    void addAtHead(int val) {
        LinkList* newNode = new LinkList(val);
        newNode->next = _dummyhead->next;
        _dummyhead->next = newNode;
        _size++;
    }
    //尾插法
    void addAtTail(int val) {
        LinkList* newNode = new LinkList(val);
        LinkList* cur = _dummyhead;
        while (cur->next != nullptr) {
            cur = cur->next;
        }
        cur->next = newNode;
        _size++;
    }
    //中间插入
    void addAtIndex(int index, int val) {
        if (index > _size) {
            return;
        }
        if (index < 0) {
            index = 0;
        }
        LinkList* newNode = new LinkList(val);
        LinkList* cur = _dummyhead;
        while (index--) {
            cur = cur->next;
        }
        newNode->next = cur->next;
        cur->next = newNode;
        _size++;
    }
    //删除第index个节点
    void deleteAtIndex(int index) {
        if (index >= _size || index < 0) {
            return;
        }
        LinkList* cur = _dummyhead;
        while (index--) {
            cur = cur->next;
        }
        LinkList* temp = cur->next;
        cur->next = cur->next->next;
        delete temp;
        temp = nullptr;
        _size--;
    }
    //打印链表
    void printLinkList() {
        LinkList* cur = _dummyhead;
        while (cur->next != nullptr) {
            cout << cur->next->val << " ";
            cur = cur->next;
        }
        cout << endl;
    }

private:
    int _size;
    LinkList* _dummyhead;
};

双链表:

class MyLinkedList {
public:
    //定义链表结构体
    struct LinkList {
        int val;
        LinkList* next;
        LinkList* prev;
        LinkList(int val) : val(val), next(nullptr), prev(nullptr) {}
    };
    //初始化链表
    MyLinkedList() {
        _dummyhead = new LinkList(0);
        _size = 0;
    }
    //获取第index个节点数据
    int get(int index) {
        //如果下标无效则返回-1
        //先判断index是否大于_size-,再判断index是否小于
        if (index > (_size - 1) || index < 0) {
            return -1;
        }
        LinkList* cur = _dummyhead->next;
        while (index--) {
            cur = cur->next;
        }
        return cur->val;
    }
    //头插法
    void addAtHead(int val) {
        LinkList* newNode = new LinkList(val);
        newNode->prev = _dummyhead;
        newNode->next = _dummyhead->next;
        //这是为了防止newNode->next为空,如果为空,则不用进行该操作
        //后面两个加“注意”的判断也是这个原因
        //调试好久才发现。。。
        if (newNode->next != nullptr) {
            newNode->next->prev = newNode;
        }
        _dummyhead->next = newNode;
        _size++;
    }
    //尾插法
    void addAtTail(int val) {
        LinkList* newNode = new LinkList(val);
        LinkList* cur = _dummyhead;
        while (cur->next != nullptr) {
            cur = cur->next;
        }
        newNode->next = cur->next;
        newNode->prev = cur;
        cur->next = newNode;
        _size++;
    }
    //中间插入
    void addAtIndex(int index, int val) {
        if (index > _size) {
            return;
        }
        if (index < 0) {
            index = 0;
        }
        LinkList* newNode = new LinkList(val);
        LinkList* cur = _dummyhead;
        while (index--) {
            cur = cur->next;
        }
        newNode->next = cur->next;
        newNode->prev = cur;
        //这里注意
        if (newNode->next != nullptr) {
            newNode->next->prev = newNode;
        }
        cur->next = newNode;
        _size++;
    }
    //删除第index个节点
    void deleteAtIndex(int index) {
        if (index >= _size || index < 0) {
            return;
        }
        LinkList* cur = _dummyhead;
        while (index--) {
            cur = cur->next;
        }
        LinkList* temp = cur->next;
        cur->next = cur->next->next;
        //这里注意
        if (temp->next != nullptr) {
            temp->next->prev = temp->prev;
        }
        delete temp;
        temp = nullptr;
        _size--;
    }
    //打印链表
    void printLinkList() {
        LinkList* cur = _dummyhead;
        while (cur->next != nullptr) {
            cout << cur->next->val << " ";
            cur = cur->next;
        }
        cout << endl;
    }

private:
    int _size;
    LinkList* _dummyhead;
};
  • 时间复杂度: 涉及 index 的相关操作为 O(index), 其余为 O(1)
  • 空间复杂度: O(n)

Leetcode 206.反转链表 

题目链接:Leetcode 206.反转链表 ​​​​​​

题目描述:反转一个单链表。

思路:一开始打算创建一个新链表从后向前赋值,但是写了很长时间,看题解发现完全没必要。

改变原来链表的指针指向不就行了吗。这里又用到之前学过的双指针法了。

首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。

然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。

最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。

动画演示看这里

第一种写法:循环迭代

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* temp;//临时保存节点
        ListNode* cur=head;
        ListNode* pre=nullptr;
        while(cur){
            temp=cur->next;//保存下一个节点
            cur->next=pre;//指针翻转
            //更新指针
            pre=cur;
            cur=temp;
        }
        return pre;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

众所周知,循环和递归经常如影随形,其本质原因是同种思路的不同写法。因此本题也可以用递归实现。

第二种写法:递归(从前向后翻转)

class Solution {
public:
    ListNode* reverse(ListNode* pre, ListNode* cur) {
        if (cur == NULL) {
            return pre;
        }
        ListNode* temp = cur->next;
        cur->next = pre;
        // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
        return reverse(cur, temp);
    }
    ListNode* reverseList(ListNode* head) {
        // 和双指针法初始化是一样的逻辑
        // ListNode* cur = head;
        // ListNode* pre = NULL;
        return reverse(NULL, head);
    }
};

第三种写法:递归(从后向前翻转)

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 边缘条件判断
        if (head == NULL)
            return NULL;
        if (head->next == NULL)
            return head;

        // 递归调用,翻转第二个节点开始往后的链表
        ListNode* last = reverseList(head->next);
        // 翻转头节点与第二个节点的指向
        head->next->next = head;
        // 此时的 head 节点为尾节点,next 需要指向 NULL
        head->next = NULL;
        return last;
    }
};
  • 时间复杂度: O(n), 要递归处理链表的每个节点
  • 空间复杂度: O(n), 递归调用了 n 层栈空间

总结:今天的题目很好的锻炼了我对链表操作的掌握,也发现了之前忽略掉的小细节(对双链表操作时要注意该节点后面是否还有节点,如果没有,则newNode->next->prev该操作不合法)

另外,如果本文有任何错误,请在评论区或私信指出,让我们共同进步!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值