203. 移除链表元素

204 阅读9分钟

Day02 2023/01/09

题目链接

难度:简单

题目

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例

输入: head = [1,2,6,3,4,5,6], val = 6
输出: [1,2,3,4,5]

思路一


这道题目解题思路还是比较常规的,即从头遍历整个链表,遇到目标节点删除即可,但是由于链表节点的 删除和插入,根据 是否带虚拟头节点 代码的实现方式比略有区别(即不带虚拟头节点的链表,删除头节点时需要额外处理一下),故有两种代码实现,但是算法思想都是一样的。

  • 不带虚拟头节点的方式
  • 带虚拟头节点的方式

思路二


由于链表这种数据结构在定义的时候就具有 天然的递归的性质,所以这道题可以考虑使用递归的解法,但是比起非递归的方式理解起来会更加的困难。实现的大概方式:

  • 首先对除了头节点 head 以外的节点进行相应的判断删除操作
  • 然后判断 head 节点,如果等于目标值 val 则需删除当前头节点,删除后当前头节点就变成了 head->next ,如果不等于目标值,则保留当前 head
  • 这里递归出口为 head 为空,下面关键点处会有解释。

关键点


  • 由于 c/c++ 的语言特性,没有 Java/Python 等语言的自动回收内存的机制,所以思路一中的两种中代码实现中都会通过代码手动释放内存。

  • 不带虚拟头节点的方式中,若要删除头节点需要单独处理,其实也十分简单,只需要让头指针向后移动一位,然后释放掉之前的头节点的内存即可。

  • 思路二中使用递归的方式,由于递归的特性,会逐步遍历链表直到最后一个节点,此时已经进入最深那层递归,所以递归出口是 head 为空,即最后一个节点之后再递归一层,就会遇到递归出口,此时就会从递归栈顶开始向下逐个执行直到执行到栈底。

扩充部分


1.单链表中如何删除一个结点

删除D节点,如图所示:

链表-删除节点

只要将C节点的next指针 指向E节点就可以了,然后再释放掉D节点所占内存。

2.怎么建立一个单链表

我相信大家都知道有两种方式:

  • 头插法(建立的链表节点是逆序排列,也可以利用这点实现元素的逆序排列
  • 尾插法(正常顺序)

但是大家应该更加熟悉 带头节点 的方式,但是对于 不带头节点 的方式可能就十分陌生,甚至根本不了解具体的代码实现,下面是两种方式不带头节点具体的代码实现。

c++代码实现-不带头节点的头插法

//采用不带头节点的头插法建立单链表(但生成的链表中结点的次序和输入的次序相反,即逆序排列)
LinkList listHeadInsert (){
    LinkList L = nullptr; //创建链表(本质是头节点指针), 初始为空
    LNode* s = nullptr; //创建待插入节点 ,初始为空
    int val = 0;//待插入节点数据 ,初始为0
    int flag = 999; //创建链表完毕标志(即输入数据999,停止节点插入,完成本次链表创建)
    cout << "请输入链表元素(输入数据999结束本次链表创建):" << endl;
    //将数据输入节点中,并将节点从链表表头插入
    while (cin >> val && val != 999) {
        cout << "请输入链表元素(输入数据999结束本次链表创建):" << endl;
        s = new LNode(val,L); //使用了带参的构造函数(即执行这两步:s->val = val,s->next = L)
        L = s; //因为采用不带头结点的头插法,所以当前插入节点在插入完毕后就会变成新的头节点。所以为了保持L始终指向头节点,所以L = s
    }
    cout << "创建结束";
     return L;//创建完毕,返回头指针
}

c++代码实现-不带头节点的尾插法

//采用不带头节点的尾插法建立单链表(生成的链表中节点次序和输入的次序相同)
LinkList listTailInsert (){
    LinkList L = nullptr; //创建链表(本质是头节点指针),初始为空
    LinkList r = nullptr; //创建尾指针,采用尾插法所以必须增加一个始终指向链表尾的指针(为了完成尾插的操作)
    LNode* s = nullptr; //创建待插入节点,初始为空
    int val = 0; //待插入节点数据 ,初始为0
    int flag = 999; //创建链表完毕标志(即输入数据999,停止节点插入,完成本次链表创建)
    cout << "请输入链表元素(输入数据999结束本次链表创建):" << endl;
    while (cin >> val && val != 999) { 
         cout << "请输入链表元素(输入数据999结束本次链表创建):" << endl;
        s = new LNode(val); //创建一个新节点(作为待插入节点)
        if(r == nullptr && L == nullptr){ //插入第一个节点的时候头尾指针都指向nullptr,这次插入需要特殊处理(因为尾插法就和我们常见的插入元素的方法是一样的,需要找到插入位置的前驱节点,但是又没有设置虚拟头节点)
            L = r = s; 
        }else { //插入了第一个节点之后就可以统一操作了。
           r->next = s;
           r = s; 
        }
    }
    return L;
}

算法实现


c++代码实现-不带虚拟头节点

// 方法一:不带虚拟头节点(即链表第一个数据节点为头节点)
#include <iostream>
#include <iterator>
#include <string>

using namespace std;

class Solution {
    //删除链表中的目标元素,并返回新头节点
    public:
        LinkList removeElements(LinkList head, int val) {
            //删除头节点(需要额外处理删除操作),注意:这里使用 while 循环,并不是 if 判断
            while(head !=nullptr && head->val == val) { //头节点不为空,而且数据为目标数值时,删除该头节点
                LinkList temp = head; //使用中间变量暂存头节点
                head = head->next; //把头结点指针向后移动一位(即删除原头节点)
                delete temp; //释放删除的头节点所占内存空间         
            }

            //删除非头节点
            LinkList cur = head; //设置一个遍历指针,初始时为当前头节点
            while (cur !=nullptr && cur->next != nullptr) { //头节点不为空,并且存在非头结点元素(即长度大于1)
                if(cur->next->val == val) { //若当前非头节点的数值为目标值,则删除
                    LinkList temp = cur->next; //使用中间变量暂存该节点
                    cur->next = cur->next->next; //让该删除节点的前驱节点的next指针指向该删除节点的后继节点(完成删除)
                    delete temp; //释放删除节点所占的内存空间
                }else {
                    cur = cur->next;
                }
            }
            return head; //返回当前最新头节点
        }
};

int main() {
    //测试一下
    cout << "输入要删除的节点数据:";
    int val = (int)cin.get();
    cin.ignore(); //跳过当前输入流中的输入的个字符,(防止尾插法中while循环cin >> val)读到之前输入流中结尾符(回车)然后返回1,直接进入循环体。(这样第一次节点数据的插入就报废了)
    Solution s1;
    //这里采用上述不带头节点的尾插法方式建立链表
    LinkList L = s1.removeElements(listTailInsert(), val);
    //要想验证结果,可以在遍历一下删除后链表的节点数据,我这里就不写了。
    return 0;
}
  • 时间复杂度 O(n)O(n) ---因为要遍历单链表,其中 n 为单链表长度
  • 空间复杂度 O(n)O(n) ---因为要创建一个单链表,其中 n 为单链表长度

c++代码实现-带虚拟头节点

#include <iostream>
#include <iterator>
#include <string>

using namespace std;

class Solution {
    public:
    //删除链表中的目标元素,并返回新头节点
    LinkList removeElements(LNode* head, int val){
        LNode* dummyHead = new LNode(0);//设置一个虚拟头节点,数据默认为0
        dummyHead->next = head; //使虚拟节点成为链表的第一个节点
        LNode* cur = dummyHead; //设置一个遍历指针,初始时为当前头节点
        while (cur->next != nullptr) {
            if(cur->next->val == val){
                LNode* temp = cur->next; //使用中间变量暂存该节点
                cur->next = cur->next->next; //让该删除节点的前驱节点的next指针指向该删除节点的后继节点(完成删除)
                delete temp;//释放删除节点所占的内存空间
            }else {
                cur = cur->next;
            }
        }
        head = dummyHead->next; //真正的头节点是虚拟头节点的下一个节点
        delete dummyHead; //释放虚拟头节点所占用内存
        return head; //返回当前最新头节点
    }
};

int main() {
    //测试一下
    cout << "输入要删除的节点数据:";
    int val = (int)cin.get();
    cin.ignore(); //跳过当前输入流中的输入的个字符,(防止尾插法中while循环cin >> val)读到之前输入流中结尾符(回车)然后返回1,直接进入循环体。(这样第一次节点数据的插入就报废了)
    Solution s1;
    //这里采用上述不带头节点的尾插法方式建立链表
    LinkList L = s1.removeElements(listTailInsert(), val); 
    //要想验证结果,可以在遍历一下删除后链表的节点数据,我这里就不写了。
    return 0;
}
  • 时间复杂度 O(n)O(n) ---因为要遍历单链表,其中 n 为单链表长度
  • 空间复杂度 O(n)O(n) ---因为要创建一个单链表,其中 n 为单链表长度

c++代码实现-递归

#include <iostream>
#include <iterator>
#include <string>

using namespace std;

//方法三:使用递归调用的方式
class Solution {
    public: 
        LinkList removeElements(LNode* head, int val) {
            if(head == nullptr) return head; //递归出口,当头节点为空时直接返回head
            head->next = removeElements(head->next, val); //使用递归调用(直到碰到递归出口开始从递归调用栈顶开始依次执行,从链表的的表位开始向前遍历并删除目标节点)
            return head->val == val ? head->next : head;       
        }
};

int main() {
    //测试一下
    cout << "输入要删除的节点数据:";
    int val = (int)cin.get();
    cin.ignore(); //跳过当前输入流中的输入的个字符,(防止尾插法中while循环cin >> val)读到之前输入流中结尾符(回车)然后返回1,直接进入循环体。(这样第一次节点数据的插入就报废了)
    Solution s1;
    //这里采用上述不带头节点的尾插法方式建立链表
    LinkList L = s1.removeElements(listTailInsert(), val); 
    //要想验证结果,可以在遍历一下删除后链表的节点数据,我这里就不写了。
    return 0;
}
  • 时间复杂度 O(n)O(n) ---取决于递归层数,其中 n 为递归层数
  • 空间复杂度 O(n)O(n) ---因为要创建一个递归调用栈,其中 n 为栈的长的

总结

  • 虽然题目简单,但是背后所涉及到的一些隐形的知识还是不要忽略,尽量的了解一下。

  • 我相信依然有一部分人对递归的实现会有困惑,对递归过程的模拟仍然有困难,下面就本题的递归解法做一个简单的模拟例如:

输入: head = [123456], val = 34
输出: [12,56]
  1. 首先执行head == nullptr判断为假。然后向下执行递归语句 head->next = removeElements(head->next, val);(从头遍历指针)。
  2. 接着不停重复步骤1,直到执行到数据为56的节点时,传入removeElements()方法中的head->nextnullptr,此时会碰到递归出口 haad == nullptr
  3. 接着返回 nullptr,回到上一层数据为56节点的代码部分,执行return head->val == val ? head->next : head; 部分的判断,然而head->next等于上一层返回的null,不等于34,所以本层会返回head,本层执行完毕,继续执行下一层。
  4. 同理执行return head->val == val ? head->next : head; 部分的判断,然而head->next等于上一层返回的 head(即数据为56的节点),不等于34,所以本层会返回head,本层执行完毕,继续执行下一层。
  5. 同理执行return head->val == val ? head->next : head; 部分的判断,然而head->next等于上一层返回的 head(即数据为34的节点),等于34,所以本层会返回head->next(即数据为56的节点),本层执行完毕,继续执行下一层。
  6. 同理执行return head->val == val ? head->next : head; 部分的判断,然而head->next等于上一层返回的 head->next(即数据为56的节点),不等于34,所以本层会返回head(即数据为12的节点),整个递归执行完毕,返回头节点head(即数据为12的节点)。

如果看完这个模拟步骤仍然懵逼的,可以结合下面的递归调用栈来分析:

递归调用栈
1. head->next = removeElements(nullptr, 34); ==nullptr 当前 head = 56
2. head->next = removeElements(56, 34); == 56 当前 head = 34
3. head->next = removeElements(34, 34) == 56; 当前 haed = 12
紧接着 3 后执行:return head->val == val ? head->next : head; 此时 head = 12不等于34,递归结束返回 head=12 这个头指针