代码随想录三 | 203.移除链表元素、707.设计链表、206.反转链表

链表理论基础

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针)。链表的入口节点称为链表的头结点也就是head。最后一个节点的指针域指向null(空指针的意思)。 

链表的类型

单链表

单链表中的指针域只能指向节点的下一个节点。

链表1

typedef struct ListNode {
    int val;  // 节点的值
    struct ListNode* next;  // 指向下一个节点的指针
} ListNode;

java 

public class ListNode {
    // 结点的值
    int val;

    // 下一个结点
    ListNode next;

    // 节点的构造函数(无参)
    public ListNode() {
    }

    // 节点的构造函数(有一个参数)
    public ListNode(int val) {
        this.val = val;
    }

    // 节点的构造函数(有两个参数)
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}
双链表

双链表:每一个节点有两个指针域,一个指向上一个节点,一个指向下一个节点。

双链表 既可以向前查询也可以向后查询。

链表2

循环链表

循环链表:链表首尾相连。

(循环链表可以用来解决约瑟夫环问题。)

链表4

链表的存储方式

数组是在内存中是连续分布的,但是链表在内存中不是连续分布的

链表是通过指针域的指针链接在内存中各个节点。链表中的节点散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

链表3

链表的操作

删除节点

删除D节点,只要将C节点的next指针指向E节点就可以了。

链表-删除节点

添加节点

链表-添加节点

性能分析

链表的增添和删除都是O(1),操作不会影响到其他节点。

但是要删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

链表的长度可以不固定,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

链表-链表与数据性能对比

文章链接

203.移除链表元素

删除链表中等于给定值 val 的所有节点。

链表操作的两种方式:

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

如果使用C,C++,要从内存中删除这两个移除的节点;如果使用java ,python不用手动管理内存 

203_链表删除元素1

直接使用原来的链表来进行移除

删除头结点和移除其他节点的操作不一样,因为删除链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。只要将头结点向后移动一位就可以。

203_链表删除元素3

203_链表删除元素4

将原头结点从内存中删掉。 

203_链表删除元素5

struct ListNode* removeElements(struct ListNode* head, int val){
    struct ListNode* temp;//定义一个指针temp
    while(head && head->val == val) {//检查头节点是否存在,并且头节点的值是否等于 val值
        temp = head;
        head = head->next;// 将新的头结点设置为head->next
        free(temp);//删除原来的头结点,释放该节点的内存空间
    }

    struct ListNode *cur = head;//定义一个指针 cur 指向头节点,用来迭代遍历链表
    while(cur && (temp = cur->next)) {// 当cur存在并且temp 指向当前节点cur的下一个节点存在时
    //(此解法需要判断cur存在因为cur指向head。若head本身为NULL或者原链表中元素都为val的话,cur也会为NULL)
        // 如果 temp 的值等于 val,说明需要删除该节点
        if(temp->val == val) {//temp->val 是指针 temp 指向的节点的值
            cur->next = temp->next;// 当前节点cur的下一个节点指向 temp 的下一个节点
            free(temp);//释放 temp 的内存
        }
        //如果 temp 的值不等于 val,则 cur 指向下一个节点。
        else
            cur = cur->next;
    }

    // 返回头结点
    return head;
}

设置一个虚拟头结点在进行删除

给链表添加一个虚拟头结点为新的头结点,原链表的所有节点就可以按照统一的方式进行移除了。

203_链表删除元素6

struct ListNode* removeElements(struct ListNode* head, int val){
    // 定义别名 ListNode 代替结构体指针 struct ListNode
    typedef struct ListNode ListNode;
    // 创建一个假头结点,方便删除头结点
    ListNode *shead;
    shead = (ListNode *)malloc(sizeof(ListNode));
    shead->next = head;
    // 定义 cur 指针指向假头结点,用于迭代遍历链表
    ListNode *cur = shead;
    // 当 cur 后面还有节点时,进行如下操作
    while(cur->next != NULL){
        // 当前节点的值等于 val 时,删除该节点
        if (cur->next->val == val){
            ListNode *tmp = cur->next;
            cur->next = cur->next->next;
            free(tmp);
        }
        // 当前节点的值不等于 val 时,将 cur 后移一位
        else{
            cur = cur->next;
        }
    }
    // 将 head 指向真正的头结点
    head = shead->next;
    // 释放假头结点的内存
    free(shead);
    // 返回处理后的链表的头结点
    return head;
}

java写法

/**
 * 添加虚节点方式
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 * @param head
 * @param val
 * @return
 */
public ListNode removeElements(ListNode head, int val) {
    if (head == null) {
        return head;
    }
    // 因为删除可能涉及到头节点,所以设置dummy节点,统一操作
    ListNode dummy = new ListNode(-1, head);
    ListNode pre = dummy;
    ListNode cur = head;
    while (cur != null) {
        if (cur.val == val) {
            pre.next = cur.next;
        } else {
            pre = cur;
        }
        cur = cur.next;
    }
    return dummy.next;
}
/**
 * 不添加虚拟节点方式
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 * @param head
 * @param val
 * @return
 */
public ListNode removeElements(ListNode head, int val) {
    while (head != null && head.val == val) {
        head = head.next;
    }
    // 已经为null,提前退出
    if (head == null) {
        return head;
    }
    // 已确定当前head.val != val
    ListNode pre = head;
    ListNode cur = head.next;
    while (cur != null) {
        if (cur.val == val) {
            pre.next = cur.next;
        } else {
            pre = cur;
        }
        cur = cur.next;
    }
    return head;
}
/**
 * 不添加虚拟节点and pre Node方式
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 * @param head
 * @param val
 * @return
 */
public ListNode removeElements(ListNode head, int val) {
    while(head!=null && head.val==val){
        head = head.next;
    }
    ListNode curr = head;
    while(curr!=null){
        while(curr.next!=null && curr.next.val == val){
            curr.next = curr.next.next;
        }
        curr = curr.next;
    }
    return head;
}

题目链接

文章链接

视频链接

707.设计链表 

在链表类中实现这些功能:

  • get(index):获取链表第index个节点的数值。
  • addAtHead(val):在链表的最前面插入一个节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):在链表的最后面插入一个节点。
  • addAtIndex(index,val):在链表第index个节点前面插入一个节点。(如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。)
  • deleteAtIndex(index):删除链表的第index个节点。

707示例

typedef struct MyLinkedList {
    int val;//存储节点的值
    struct MyLinkedList* next;//指向下一个节点的指针
}MyLinkedList;

MyLinkedList* myLinkedListCreate() {
    //这个题必须用虚拟头指针,参数都是一级指针,头节点确定后没法改指向了!!!
    MyLinkedList* head = (MyLinkedList *)malloc(sizeof (MyLinkedList));//为头结点分配内存空间
    head->next = NULL;//当前链表为空
    return head;
}

int myLinkedListGet(MyLinkedList* obj, int index) {//MyLinkedList* obj:表示要操作的链表
    MyLinkedList *cur = obj->next;  // 定义指针 cur,指向链表的第一个真正存储数据的节点(实际上是头结点的下一个节点,头结点 obj 并不存储任何数据,它的作用只是作为链表的起点)
    for (int i = 0; cur != NULL; i++){  // 循环遍历链表
        if (i == index){  // 如果当前遍历到的节点正好是需要获取的节点
            return cur->val;  // 返回该节点的值,cur->val 表示当前节点 cur 的值
        }
        else{  // 否则继续遍历下一个节点
            cur = cur->next;
        }
    }
    return -1;  // 遍历完整个链表仍然没有找到需要获取的节点,则说明索引不合法,返回 -1
}

void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
    MyLinkedList *nhead = (MyLinkedList *)malloc(sizeof (MyLinkedList));  // 创建一个新节点 nhead,并分配内存空间
    nhead->val = val;  // 设置新节点 nhead 的值为传入的参数 val。(nhead->val 是值)
    nhead->next = obj->next;  // 将新节点 nhead 的 next 指针指向链表原来的第一个节点。(nhead->next是指针)
    obj->next = nhead;  // 将头结点 obj 的 next 指针指向新节点 nhead,将新节点插入到链表的头部
}

void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
    MyLinkedList *cur = obj;  // 定义一个指针变量 cur,初始化为头结点 obj
    while(cur->next != NULL){  // 遍历链表,找到最后一个节点
        cur = cur->next;
    }
    MyLinkedList *ntail = (MyLinkedList *)malloc(sizeof (MyLinkedList)); //创建一个新的节点 ntail,并为其分配内存空间
    ntail->val = val;  // 设置新节点 ntail 的值为传入的参数 val
    ntail->next = NULL;  // 将新节点 ntail 的 next 指针设置为 NULL,表示它是链表的最后一个节点
    cur->next = ntail;  // 将当前节点 cur 的 next 指针指向新节点 ntail,将新节点插入到链表的尾部
}

void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
    if (index == 0){  // 如果 index 等于 0,将新节点添加到链表头部
        myLinkedListAddAtHead(obj, val);  // 调用 myLinkedListAddAtHead() 函数,将新节点添加到链表头部
        return;  // 添加完成后直接返回
    }
    MyLinkedList *cur = obj->next;  // 定义一个指针变量 cur,初始化为头结点的下一个节点
    for (int i = 1 ;cur != NULL; i++){  // 遍历链表,查找要插入的位置
        if (i == index){  // 如果找到了要插入的位置
            MyLinkedList* newnode = (MyLinkedList *)malloc(sizeof (MyLinkedList));  // 创建一个新的节点 newnode,并为其分配内存空间
            newnode->val = val;  // 将新节点的值设置为传入的参数 val
            newnode->next = cur->next;  // 将新节点的 next 指针指向当前节点的下一个节点
            cur->next = newnode;  // 将当前节点的 next 指针指向新节点,将新节点插入到链表中间
            return;  // 插入完成后直接返回
        }
        else{  // 如果还没有找到要插入的位置
            cur = cur->next;  // 将指针 cur 指向下一个节点,继续遍历链表
        }
    }
}

void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
    if (index == 0){  // 如果 index 等于 0,删除链表中的第一个节点
        MyLinkedList *tmp = obj->next;  // 定义一个指针变量 tmp,指向链表中的第一个节点
        if (tmp != NULL){  // 如果链表不为空
            obj->next = tmp->next;  // 将头结点的 next 指针指向第二个节点
            free(tmp);  // 释放第一个节点的内存空间
        }
        return;  // 删除完成后直接返回
    }
    MyLinkedList *cur = obj->next;  // 定义一个指针变量 cur,初始化为头结点的下一个节点
    for (int i = 1 ;cur != NULL && cur->next != NULL; i++){  // 遍历链表,查找要删除的节点
        if (i == index){  // 如果找到了要删除的节点
            MyLinkedList *tmp = cur->next;  // 定义一个指针变量 tmp,指向要删除的节点
            if (tmp != NULL) {  // 如果要删除的节点存在
                cur->next = tmp->next;  // 将当前节点的 next 指针指向要删除节点的下一个节点
                free(tmp);  // 释放要删除节点的内存空间
            }
            return;  // 删除完成后直接返回
        }
        else{  // 如果还没有找到要删除的节点
            cur = cur->next;  // 将指针 cur 指向下一个节点,继续遍历链表
        }
    }
}

void myLinkedListFree(MyLinkedList* obj) {
    while(obj != NULL){  // 循环遍历链表中的每个节点
        MyLinkedList *tmp = obj;  // 定义一个指针变量 tmp,指向当前节点
        obj = obj->next;  // 将头结点指针指向下一个节点,准备删除当前节点
        free(tmp);  // 释放当前节点的内存空间
    }
}  // 释放所有节点的内存空间,销毁链表

java写法

//单链表
class ListNode {
    int val;
    ListNode next;
    ListNode(){}
    ListNode(int val) {
        this.val=val;
    }
}
class MyLinkedList {
    //size存储链表元素的个数
    int size;
    //虚拟头结点
    ListNode head;

    //初始化链表
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);
    }

    //获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
    public int get(int index) {
        //如果index非法,返回-1
        if (index < 0 || index >= size) {
            return -1;
        }
        ListNode currentNode = head;
        //包含一个虚拟头节点,所以查找第 index+1 个节点
        for (int i = 0; i <= index; i++) {
            currentNode = currentNode.next;
        }
        return currentNode.val;
    }

    //在链表最前面插入一个节点,等价于在第0个元素前添加
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    //在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

    // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果 index 大于链表的长度,则返回空
    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        }
        if (index < 0) {
            index = 0;
        }
        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个节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        size--;
        if (index == 0) {
            head = head.next;
	    return;
        }
        ListNode pred = head;
        for (int i = 0; i < index ; i++) {
            pred = pred.next;
        }
        pred.next = pred.next.next;
    }
}

//双链表
class ListNode{
    int val;
    ListNode next,prev;
    ListNode() {};
    ListNode(int val){
        this.val = val;
    }
}


class MyLinkedList {  

    //记录链表中元素的数量
    int size;
    //记录链表的虚拟头结点和尾结点
    ListNode head,tail;
    
    public MyLinkedList() {
        //初始化操作
        this.size = 0;
        this.head = new ListNode(0);
        this.tail = new ListNode(0);
        //这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!!
        head.next=tail;
        tail.prev=head;
    }
    
    public int get(int index) {
        //判断index是否有效
        if(index<0 || index>=size){
            return -1;
        }
        ListNode cur = this.head;
        //判断是哪一边遍历时间更短
        if(index >= size / 2){
            //tail开始
            cur = tail;
            for(int i=0; i< size-index; i++){
                cur = cur.prev;
            }
        }else{
            for(int i=0; i<= index; i++){
                cur = cur.next; 
            }
        }
        return cur.val;
    }
    
    public void addAtHead(int val) {
        //等价于在第0个元素前添加
        addAtIndex(0,val);
    }
    
    public void addAtTail(int val) {
        //等价于在最后一个元素(null)前添加
        addAtIndex(size,val);
    }
    
    public void addAtIndex(int index, int val) {
        //index大于链表长度
        if(index>size){
            return;
        }
        //index小于0
        if(index<0){
            index = 0;
        }
        size++;
        //找到前驱
        ListNode pre = this.head;
        for(int i=0; i<index; i++){
            pre = pre.next;
        }
        //新建结点
        ListNode newNode = new ListNode(val);
        newNode.next = pre.next;
        pre.next.prev = newNode;
        newNode.prev = pre;
        pre.next = newNode;
        
    }
    
    public void deleteAtIndex(int index) {
        //判断索引是否有效
        if(index<0 || index>=size){
            return;
        }
        //删除操作
        size--;
        ListNode pre = this.head;
        for(int i=0; i<index; i++){
            pre = pre.next;
        }
        pre.next.next.prev = pre;
        pre.next = pre.next.next;
    }
}

题目链接

文章链接

视频链接

206.反转链表 

输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

如果再定义一个新的链表,实现链表元素的反转,会对内存空间造成浪费。

其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。

206_反转链表

动画所示:(纠正:动画应该是先移动pre,在移动cur)

 双指针法

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

然后开始反转,把 cur->next 节点用temp指针保存一下(保存该节点呢,因为要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点)。

接下来,循环遍历,继续移动pre和cur指针。

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

  • 时间复杂度: O(n)
  • 空间复杂度: O(1)
struct ListNode* reverseList(struct ListNode* head){
    //保存cur的下一个结点
    struct ListNode* temp;
    //pre指针指向前一个当前结点的前一个结点
    struct ListNode* pre = NULL;
    //用head代替cur,也可以再定义一个cur结点指向head。
    while(head) {
        //保存下一个结点的位置
        temp = head->next;
        //翻转操作
        head->next = pre;
        //更新结点
        pre = head;
        head = temp;
    }
    return pre;
}

java写法

// 双指针
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode cur = head;
        ListNode temp = null;
        while (cur != null) {
            temp = cur.next;// 保存下一个节点
            cur.next = prev;
            prev = cur;
            cur = temp;
        }
        return prev;
    }
}

递归法 

将链表的反转拆分成两部分(反转前一个节点和当前节点的指针,以及反转当前节点和下一个节点的指针),通过递归遍历链表,在遍历到当前节点时完成反转操作,并不断返回反转后的链表头结点(在递归过程中,链表的头结点可能会发生变化),最终得到完整的反转链表。 

  • 时间复杂度: O(n), 要递归处理链表的每个节点
  • 空间复杂度: O(n), 递归调用了 n 层栈空间
struct ListNode* reverse(struct ListNode* pre, struct ListNode* cur) {
    if(!cur)
        return pre;
    struct ListNode* temp = cur->next;  // 创建一个临时指针temp,保存当前节点cur的下一个节点
    cur->next = pre;  // 将当前节点cur的next指针指向pre,实现反转操作
    // 将cur作为pre传入下一层递归函数调用
    // 将temp作为cur传入下一层递归函数调用,并改变temp的next指针指向当前cur,将temp连接到反转后的链表上
    return reverse(cur, temp);  // 递归调用reverse函数
}

struct ListNode* reverseList(struct ListNode* head){
    return reverse(NULL, head);  // 调用辅助函数reverse来完成整个链表的反转,初始时将pre设置为NULL,即空链表,将head作为第二个节点传入reverse函数
}

 

java写法

// 递归 
class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }

    private ListNode reverse(ListNode prev, ListNode cur) {
        if (cur == null) {
            return prev;
        }
        ListNode temp = null;
        temp = cur.next;// 先保存下一个节点
        cur.next = prev;// 反转
        // 更新prev、cur位置
        // prev = cur;
        // cur = temp;
        return reverse(cur, temp);
    }
}
// 从后向前递归
class Solution {
    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;
    } 
}

题目链接 

文章链接 

视频链接 

 

  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值