给自己复盘的随想录笔记-链表

链表

定义

数字域和指针域

种类

单链表,双链表,循环链表

链表的存储方式

链表是通过指针域的指针链接在内存中各个节点。

所以链表中的节点在内存中不是连续分布 的 ,而是散乱 分布在内存中的某地址上,分配机制取决于操作系统的内存管理

删除链表

增加链表

与数组的对比

再把链表的特性和数组的特性进行一个对比,如图所示:

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

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

移除链表元素

移除操作,就是让节点next指针直接指向下下一个节点就可以了,

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

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

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

这里我选择第二种方式,更好理解和记忆;

给链表添加一个虚拟头结点为新的头结点,此时要移除这个旧头结点元素1。

这样是不是就可以使用和移除链表其他节点的方式统一了呢?

来看一下,如何移除元素1 呢,还是熟悉的方式,然后从内存中删除元素1。

最后呢在题目中,return 头结点的时候,别忘了return dummyNode->next;, 这才是新的头结点

java 复制代码
/**
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 * @param head
 * @param val
 * @return
 */
public ListNode removeElements(ListNode head, int val) {
    // 设置一个虚拟的头结点
    ListNode dummy = new ListNode();
    dummy.next = head;

    ListNode cur = dummy;
    while (cur.next != null) {
        if (cur.next.val == val) {
            cur.next = cur.next.next;
        } else {
            cur = cur.next;        
        }
    }
    return dummy.next;
}

设计链表

设计的为单链表时

我分析这个代码的思路,因为是链表,内存空间不是连续的,所以对那个Index的理解不明确;

而且又是单链表,只有一个next指针,所以分析的时候要注意什么找到什么链表

707. 设计链表 - 力扣(LeetCode)

思路

这是链表最基础的操作,牢记于心!

首先初始化:由于给的是链表:MyLinkedList ,需要初始化size(长度),listnode(头节点)。而题目没有给出listnode的定义,需要自己定义一个ListNode类,记得定义构造函数,不然在MyLinkedList初始化的时候无法直接赋值。

1.核心是addAtIndex(int index, int val);需要判断index,如果大于size,返回;

如果<0,令其=0;

执行插入,由于是index的前面插入 ,所以要找到index的前驱节点循环次数=index ;插入时先将新节点与pre.next连接,再将pre.next=新节点。 这样是为了防止先执行 pre.next=新节点,会断链找不到后面的链表。

2.addAtHead,addAtTail直接调用addAtIndex即可。

3.deleteAtIndex(int index),首先找到index的前驱节点 ,然后将pre.next=pre.next.next即可。

4.get(int index):首先进行有效值判断,然后for循环即可。这里不需要找到前驱节点因此循环次数是Index+1.

解题过程

刚开始用的是while循环,每次pre指针往后移动的时候,Index--;这样肯定没有for循环看着舒适。而且没有搞明白什么时候需要找到先驱节点(插入,删除) ,以及循环多少次可以找到先驱节点(当pre=head的时候,循环次数=index)。以及不会写列表的构造函数(size,listnode),不过我现在都会啦~!

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;
    }

    public void addAtHead(int val) {
        ListNode newNode = new ListNode(val);
        newNode.next = head.next;
        head.next = newNode;
        size++;

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

    
    public void addAtTail(int val) {
        ListNode newNode = new ListNode(val);
        ListNode cur = head;
        while (cur.next != null) {
            cur = cur.next;
        }

        cur.next = newNode;
        size++;

        // 在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
        // 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--;
        //因为有虚拟头节点,所以不用对Index=0的情况进行特殊处理
        ListNode pred = head;
        for (int i = 0; i < index ; i++) {
            pred = pred.next;
        }
        pred.next = pred.next.next;
    }
}

当设计的为双链表时

java 复制代码
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>=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;
        }

        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>=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;
    }
}

/**
 * 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. 反转链表 - 力扣(LeetCode)

方法一:双指针法

思路分析:

java 复制代码
class Solution {
	public ListNode reverseList(ListNode head) {
		//申请节点,pre和 cur,pre指向null
		ListNode pre = null;
		ListNode cur = head;
		ListNode tmp = null;
		while(cur!=null) {
			//记录当前节点的下一个节点
			tmp = cur.next;
			//然后将当前节点指向pre
			cur.next = pre;
			//pre和cur节点都前进一位
			pre = cur;
			cur = tmp;
		}
		return pre;
	}
}

方法二:递归的方法

206. 反转链表 - 力扣(LeetCode)

看懂了题解,但是自己能不能写出来是一个问题

多国几遍吧这个题目

java 复制代码
class Solution {
    public ListNode reverseList(ListNode head) {
        return recur(head, null);    // 调用递归并返回
    }
    private ListNode recur(ListNode cur, ListNode pre) {
        if (cur == null) return pre; // 终止条件
        ListNode res = recur(cur.next, cur);  // 递归后继节点
        cur.next = pre;              // 修改节点引用指向
        return res;                  // 返回反转链表的头节点
    }
}

两两交换链表中的节点

24. 两两交换链表中的节点 - 力扣(LeetCode)

这个思路比递归好理解,借助了一个stack

我们利用一个 stack,然后不断迭代链表,每次取出两个节点放入 stack 中,再从 stack 中拿出两个节点。

借助 stack 后进先出的特点,放进去的时候是 1,2 。拿出来的时候就是 2,1 两个节点了。

再把这两个节点串联起来,重复这个逻辑遍历完整个链表,就可以做到两两反转的效果了。

虽然用到了 stack,但因为只存了两个元素,所以空间复杂度还是 O(1),时间复杂度是 O(n)。

java 复制代码
class Solution {
	public ListNode swapPairs(ListNode head) {
		if(head==null || head.next==null) {
			return head;
		}
		//用stack保存每次迭代的两个节点
		Stack<ListNode> stack = new Stack<ListNode>();
		ListNode p = new ListNode(-1);
		ListNode cur = head;
		//head指向新的p节点,函数结束时返回head.next即可
		head = p;
		while(cur!=null && cur.next!=null) {
			//将两个节点放入stack中
			stack.add(cur);
			stack.add(cur.next);
			//当前节点往前走两步
			cur = cur.next.next;
			//从stack中弹出两个节点,然后用p节点指向新弹出的两个节点
			p.next = stack.pop();
			p = p.next;
			p.next = stack.pop();
			p = p.next;
		}
		//注意边界条件,当链表长度是奇数时,cur就不为空
		if(cur!=null) {
			p.next = cur;
		} else {
			p.next = null;
		}
		return head.next;
	}
}

递归

24. 两两交换链表中的节点 - 力扣(LeetCode)

终于看懂这个关于递归的题目解析,但是要是我自己写出来还是有难度的

解题思路

标签:链表

本题的递归和非递归解法其实原理类似,都是更新每两个点的链表形态完成整个链表的调整

其中递归解法可以作为典型的递归解决思路进行讲解

递归写法要观察本级递归的解决过程,形成抽象模型,因为递归本质就是不断重复相同的事情。而不是去思考完整的调用栈,一级又一级,无从下手。如图所示,我们应该关注一级调用小单元的情况,也就是单个 f(x)。

其中我们应该关心的主要有三点:

返回值

调用单元做了什么

终止条件

在本题中:

返回值:交换完成的子链表

调用单元:设需要交换的两个点为 head 和 next,head 连接后面交换完成的子链表,next 连接 head,完成交换

终止条件:head 为空指针或者 next 为空指针,也就是当前无节点或者只有一个节点,无法进行交换

java 复制代码
class Solution {
    public ListNode swapPairs(ListNode head) {
        if(head == null || head.next == null){
            return head;
        }
        ListNode next = head.next;
        head.next = swapPairs(next.next);
        next.next = head;
        return next;
    }
}
相关推荐
李小白664 分钟前
二叉树的练习题(中)
java·数据结构·算法
骑鱼过海的猫1237 分钟前
【redis】redis
java·数据库·redis
漫天转悠7 分钟前
Java21和Java8性能优化详细对比
java
y250816 分钟前
《抽象类和接口》
java·开发语言
脸红ฅฅ*的思春期18 分钟前
Java安全—log4j日志&FastJson序列化&JNDI注入
java·安全·log4j·fastjson·jndi注入
NMBG2233 分钟前
[JAVAEE] 网络编程
java·服务器·网络·tcp/ip·udp·java-ee
chunmiao303234 分钟前
自建k8s集群,利用开源的GitLab、Jenkins和Harbor实现CI/CD和DevOps的过程回顾
java·开发语言
gugugu.35 分钟前
数据结构:图(一)---- 图的基础和遍历
数据结构
无敌最俊朗@36 分钟前
c#————委托Action使用例子
java·前端·c#
single5941 小时前
【c++笔试强训】(第五篇)
java·开发语言·c++·vscode·学习·算法·牛客