【算法通关村】链表基础经典问题解析

news2025/2/27 10:04:59

【算法通关村】链表基础&经典问题解析

一.什么是链表

链表是一种通过指针将多个节点串联在一起的线性结构每一个节点(结点)都由两部分组成,一个是数据域(用来存储数据),一个是指针域(存放指向下一个节点的地址),最后一个节点的指针域指向null(空指针的意思)。
在这里插入图片描述

链表的入口节点称为链表的头结点。普通的单链表就是只给你一个指向链表头的指针head,如果想访问其他元素,就只能从head开始一个个向后找,遍历链表,最终会在访问尾结点之后如果继续访问,就会返回null。

从上图看,是不是觉得链表的存储方式有点像数组?实际上链表的各节点在内存中的存储次序是混乱的,它是通过指针域中的指针链接在内存中各个节点,即每个节点中都有记录它所链接的节点的地址。
在这里插入图片描述

二.链表的创建

  1. 要想创建链表,我们就必须先构建链表中的每个节点:
public class Node {
   public Object data;  // 存储数据
   public Node next;	// 存储所连接的下一个节点的地址
    
   Node(Object data) {
        this.data = data;
        this.next = null;
    }
}
  1. 接下来我们就可以创建链表结构:
public class Code {
    public static void main(String[] args) {
        Node head = new Node(1);  // 创建头节点
        Node node1 = new Node(2); // 创建节点1
        head.next = node1;        // 令头节点指向节点1
        Node node2 = new Node(3); // 创建节点2
        node1.next = node2;       // 令节点1指向节点2
        Node node3 = new Node(4); // 创建节点3
        node2.next = node3;       // 令节点2指向节点3
        Node node4 = new Node(5); // 创建节点4
        node3.next = node4;       // 令节点3指向节点4
    }
}
  1. 通过debug调试我们可以看到已经成功创建出了链表结构:

在这里插入图片描述

三.链表的增删改查

(1) 遍历链表

由于链表的各节点在内存中的存储次序是混乱的,我们一般只知道它的头节点,因此对于单链表,不管进行什么操作,一定是从头开始逐个向后访问,直到null为止。所以操作之后是否还能找到表头节点非常重要,千万不要只顾当前位置而将标记表头的指针丢掉了。

   /**
     * 遍历链表数据域内容并返回链表长度
     *
     * @param head 链表头节点
     * @return 链表的长度
     */
    public static int getLengthAndShow(Node head) {
        int length = 0;
        Node node = head;  // 拷贝一份头节点地址进行操作,避免头节点指针丢失
        while (node != null) {
            System.out.print(node.data + "->");
            length++;
            node = node.next; // 根据指针域找到下一个节点
        }
        return length;
    }

示意图如下:
在这里插入图片描述

(2) 链表的插入

单链表的插入操作需要考虑三种情况:首部插入、中部插入和尾部插入

(2.1) 首部插入

链表的首部插入非常简单,我们只需要将待插入节点的指针域指向头节点,然后更新头节点为待插入的节点即可

    /**
     * 插入节点在链表首部
     *
     * @param head       头节点
     * @param insertNode 待插入的节点
     * @return 新的头节点
     */
    public static Node insertTop(Node head, Node insertNode) {
        insertNode.next = head; // 令插入的节点指向头节点
        return insertNode;  // 返回新的头节点
    }

示例图如下:

在这里插入图片描述

(2.2) 中部插入

链表的中部插入会相对麻烦一点,首先我们必须先遍历找到要插入的位置前驱节点,然后在当前位置与前驱结点和后继结点进行连接

为什么要找到前驱节点而不是待插入的位置呢?这是由于链表的结构使然,链表的遍历就如时间的流逝,只能向后,无法回溯。如果我们遍历到待插入的位置,若未记录前驱节点,那么我们将无法再获取到前驱节点!

 /**
     * 插入节点在链表中部
     *
     * @param head       头节点
     * @param insertNode 待插入的节点
     * @param pos        插入的位置(从0开始)
     *                   (此处省略pos位于首部和尾部的处理,只考虑中部)
     */
    public static void insertMiddle(Node head, Node insertNode, int pos) {
        // 1.找到插入位置的前驱节点
        while (pos > 1) {
            head = head.next;
            pos--;
        }
        // 2.先令待插入的节点指向后继节点
        insertNode.next = head.next;
        // 3.再令前驱节点指向待插入的节点
        head.next = insertNode;
    }

示意图如下:
在这里插入图片描述

为什么要先令待插入的节点指向后继节点再让前驱节点指向待插入的节点呢?
这是由于每个节点都只有一个next(即只能保存一份地址),倘若我们先令前驱节点指向待插入的节点,那么原本前驱节点与后继节点之间的链接将会断开,我们将丢失后续的节点。

(2.3) 尾部插入

尾部插入也是比较简单的,我们只需要遍历到尾部节点,再令尾部节点指向待插入的节点即可

    /**
     * 插入节点在链表尾部
     *
     * @param head       头节点
     * @param insertNode 待插入的节点
     */
    public static void insertEnd(Node head, Node insertNode) {
        // 1.找到尾部位置
        while (head.next != null) {
            head = head.next;
        }
        // 2.令尾部节点指向待插入的节点
        head.next = insertNode;
    }

示意图如下:

在这里插入图片描述

(3) 链表的删除

与插入情况类似,单链表的删除操作也需要考虑三种情况:首部删除、中部删除和尾部删除

(3.1) 首部删除

删除头节点非常的简单,只需要将当前头节点更新为当前头节点指针域所指向的地址即可。 一般只要执行head=head.next即可。

    /**
     * 删除的节点在链表首部
     *
     * @param head 头节点
     * @return 新的头节点
     */
    public static Node removeTop(Node head) {
        head = head.next; // 将当前头节点更新为当前头节点指针域所指向的地址
        return head;
    }

示意图如下:

在这里插入图片描述

(3.2) 中部删除

删除中间结点与插入类似,也需要我们先找到待删除位置的前驱节点,然后再将前驱节点的指针域改为后继节点指针域中所保存的地址。

   /**
     * 删除的节点在链表中部
     *
     * @param head 头节点
     * @param pos  插入的位置(从0开始)
     *             (此处省略pos位于首部和尾部的处理,只考虑中部)
     */
    public static void removeMiddle(Node head, int pos) {
        // 1.找到删除位置的前驱节点
        while (pos > 1) {
            head = head.next;
            pos--;
        }
        // 2.将前驱节点的指针域改为后继节点指针域中所保存的地址
        head.next = head.next.next;
    }

示意图如下:

在这里插入图片描述

(3.3) 尾部删除

要删除尾部节点同样要找到尾部节点的前驱节点,如果我们知道尾部节点的位置,那么尾部节点的删除其实与中部节点的删除方法一致,否则我们便需要通过cur.next.next == null来判断了。

    /**
     * 删除的节点在链表尾部
     *
     * @param head 头节点
     */
    public static void removeEnd(Node head) {
        // 1.找到删除位置的前驱节点
        while (head.next.next != null) {
            head = head.next;
        }
        // 2.将前驱节点的指针域改为后继节点指针域中所保存的地址
        head.next = head.next.next; // 或者 head.next = null
    }

示意图如下:

在这里插入图片描述

四.双向链表

(1) 基本概念

上面我们所讲解的链表都为单向链表,即链表之间是单向连接的,都由前一个节点指向后一个节点,我们只能从某一个节点开始获取到它后面的节点,而无法获取到它前面的节点

双向链表顾名思义就是既可以向前,也可以向后,即链表之间是相互连接的,我们即可从某一个节点开始获取到它后面的节点,也可以获取到它前面的节点。有两个指针的好处自然是移动元素更方便。

在这里插入图片描述

(2) 创建

双向链表的节点定义如下:

class DoubleListNode {
    public int data;    //数据域
    public DoubleListNode next;    //指向下一个结点
    public DoubleListNode prev;    //指向上一个结点
    public DoubleNode(int data) {
        this.data = data;
    }
}

其遍历与单向链表非常相似便不赘述。

(3) 插入

(3.1) 首尾插入

在首部和尾部插入比较容易且类似:

// 首部插入
public void insertFirst(int data) {
    DoubleNode newDoubleNode = new DoubleNode(data);
    if (first == null) {
        last = newDoubleNode;
    } else {
        // 将第一个结点的prev指向newNode
        first.prev = newDoubleNode;  
        // 将还未插入节点结点的next指向第一个节点
        newDoubleNode.next = first;
    }
    //将新结点赋给first成为第一个结点
    first = newDoubleNode;            
}
// 尾部插入
public void insertLast(int data) {
    DoubleNode newDoubleNode = new DoubleNode(data);
    if (first == null) {
        first = newDoubleNode;        
    } else {
        // 将最后一个结点的next指向newNode
        last.next = newDoubleNode;    
        // 将还未插入节点结点的prev指向最后一个节点
        newDoubleNode.prev = last;
    }
    // 将新结点赋给last成为最后一个结点
    last = newDoubleNode;                
}

示意图如下:

在这里插入图片描述

(3.2) 中间插入

由于双向链表的特性,中间插入节点方法不唯一,总之都需要找到要插入位置的前驱节点或者后继节点。

在这里插入图片描述

(4) 删除

(4.1) 首尾删除

在首部和尾部删除比较容易且类似:

//删除首元素
public void deleteFirst() {
    //若链表只有一个结点,删除后链表为空,将last指向null
    if (first.next == null) {            
        last = null;
    } else {
        //若链表有两个及以上的结点 ,因为是头部删除,则first.next将变成第一个结点,其previous将变成null
        first.next.prev = null;    
        //将first.next赋给first
        first = first.next; 
    }                  
}

//从尾部删除结点
public DoubleNode deleteLast() {
    //如果链表只有一个结点,则删除以后为空表,last指向null
    if (first.next == null) {        
        first = null;
    } else {
        //将上一个结点的next域指向null
        last.prev.next = null;   
        //上一个结点称为最后一个结点,last指向它
        last = last.prev;    
    }   
}

示意图如下:

在这里插入图片描述

(4.2) 中间删除

由于双向链表的特性,中间删除节点的方法也不唯一。

示意图如下:

在这里插入图片描述

五.经典问题分析

(1) 链表相交

力扣链接:链表相交

在这里插入图片描述

(1.1) Hash

我们可以先将任意一个链表节点全部加入到集合中,再遍历另一个链表比较该节点是否包含在集合中,如果包含则代表它就是第一个公共子节点,中止循环,否则表示两个链表没有公共子节点。

    public Node getIntersectionNode(Node headA, Node headB) {
        HashSet<Node> nodes = new HashSet<>(); 
        Node ans = null;
        // 1.将其中一个链表节点全部加入到集合
        while (headA != null) {
            nodes.add(headA);
            headA = headA.next;
        }
        // 2.遍历另一个链表
        while (headB != null) {
            // 3.判断该节点是否包含在集合中
            if (nodes.contains(headB)) {
                // 4.如果包含则代表该节点就是第一个公共子节点
                ans = headB;
                // 5.中止循环
                break;
            }
            headB = headB.next;
        }
        return ans;
    }

(1.2) 栈

如果两个链表有公共子节点,那么它的第一个公共子节点以后的节点都应该是相同的,我们可以先将两个链表的所有节点都分别压入栈中,再依次出栈进行比较,如果相等则继续出栈,一直找到最晚出栈的那一组,可以得知最后出栈的一对相等节点为第一个公共子节点

    public Node getIntersectionNode(Node headA, Node headB) {
        Node ans = null;
        Stack<Node> aS = new Stack<>();
        Stack<Node> bS = new Stack<>();
        // 1.将两个链表分别压入栈中
        while (headA != null) {
            aS.push(headA);
            headA = headA.next;
        }
        while (headB != null) {
            bS.push(headB);
            headB = headB.next;
        }
        // 2.依次出栈进行比较
        while (aS.size() > 0 && bS.size() > 0) {
            Node aN = aS.pop();
            Node bN = bS.pop();
            // 3.当碰到第一对不相等的节点时,可以得出上一对出栈的节点为第一个公共子节点
            if (!aN.equals(bN)) {
                break;
            }
            ans = aN;
        }
        return ans;
    }

(1.3) 双指针

由于第一个公共子节点可能距离两条链表的头节点不同距离,我们可以先计算出两个链表的长度差,令较长的链表先移动长度差距离,使得两个链表的起始同步遍历位置与第一个公共子节点距离相同,然后我们再同步遍历两条链表,比较各个节点,判断是否相等,当碰到第一个相等的子节点则表示第一个公共子节点。

public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {
    if(pHead1==null || pHead2==null){
        return null;
    }
    // 1.创建操作新节点,避免丢失头节点地址
    ListNode cur1=pHead1;
    ListNode cur2=pHead2;
    int l1=0,l2=0;
    // 2.分别统计两个链表的长度
    while(cur1!=null){
        cur1=cur1.next;
        l1++;
    }
    while(cur2!=null){
        cur2=cur2.next;
        l2++;
    }
    // 3.计算两条链表的长度差
    int sub=l1>l2?l1-l2:l2-l1;
    // 重新获取头节点地址
    cur1=pHead1;
    cur2=pHead2;
    // 4.令较长的链表先走sub步
    if(l1>l2){
        int a=0;
        while(a<sub){
            cur1=cur1.next;
            a++;
        }   
    }
    if(l1<l2){
        int a=0;
        while(a<sub){
            cur2=cur2.next;
            a++;
        }   
    }
    // 5.同时遍历两个链表,当碰到第一个相等的子节点则表示第一个公共子节点
    while(cur2!=cur2){
        cur2=cur2.next;
        cur1=cur1.next;
    } 
    return cur1;
}

(2) 回文链表

力扣链接:回文链表

在这里插入图片描述

(2.1) 集合+双指针

我们可以先将链表中的所有数据添加到集合中,再针对集合同时从首尾遍历比较数据是否相同。

   public boolean isPalindrome(ListNode head) {
       // 1.全部添加到集合中
       ArrayList<Integer> list = new ArrayList<>();
        while (head != null) {
            list.add(head.val);
            head = head.next;
        }
        // 2.左右同时开始遍历比较是否相同
        int last = list.size() - 1;
        int start = 0;
        while (start <= last) {
            // 3.如果发现不同则代表不是回文数组
            if (!Objects.equals(list.get(start), list.get(last))) {
                return false;
            }
            start++;
            last--;
        }
        return true;
    }

(2.2) 栈

我们也可以先将链表中的所有数据添加到栈中,再次遍历链表同时进行出栈,比较数据是否相同。

    public boolean isPalindrome(ListNode head) {
        ListNode temp = head;
        Stack<Integer> stack = new Stack();
        // 1.把链表节点的值存放到栈中
        while (temp != null) {
            stack.push(temp.val);
            temp = temp.next;
        }
        // 2.之后一边出栈,一遍比较
        while (head != null) {
            if (head.val != stack.pop()) {
                return false;
            }
            head = head.next;
        }
        return true;
    }

(3) 合并有序链表合集

(3.1) 合并两个有序链表

力扣链接:合并两个有序链表

在这里插入图片描述

题解:我们可以先创建一个新的链表用来保存结果,再逐个比较每个节点的数值,使用新的链表链接较小值节点,并令较小值节点链表指针移动,当有一方为null时,停止比较,令新链表链接上另一条链表。

  public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
       // 创建一个虚拟头节点用于标识新链表的开始
       ListNode dummyNode = new ListNode(-1);
       // 重新创捷一个节点用于链接,避免新链表头节点指针丢失
       ListNode head = dummyNode;
       // 一直逐个进行比较,直到一条链表为空
        while (list1 != null && list2 != null) {
            // 连接上较小值节点,并使其链表指针后移一个
            if (list1.val > list2.val) {
                head.next = list2;
                list2 = list2.next;
            } else {
                head.next = list1;
                list1 = list1.next;
            }
            // 让新链表指针后移,随时准备连接新节点
            head = head.next;
        }
      	// 至多一条链表为null,并且不为null的链表都为较大值,直接连接上即可
        head.next = list1 == null ? list2 : list1;
        // 虚拟节点仅用于链接,next才为真正有效的节点
        return dummyNode.next;
    }

(3.2) 合并k个有序链表

力扣链接:合并 K 个有序链表

在这里插入图片描述

目前我们只在此利用最暴力的解法,要求合并N个,我们是不是可以延伸上一题合并两条的解法,即将合并k条转换为两条两条合并逐渐合并?

   public  ListNode mergeKLists(ListNode[] lists) {
        ListNode dummyNode = null;
       	// 拆分为两条两条开始合并
        for (ListNode list : lists) {
           dummyNode = mergeTwoLists(dummyNode, list);
        }
        return dummyNode;
    }

    public  ListNode mergeTwoLists(ListNode list1, ListNode list2) {
       // 创建一个虚拟头节点用于标识新链表的开始
       ListNode dummyNode = new ListNode(-1);
       // 重新创捷一个节点用于链接,避免新链表头节点指针丢失
       ListNode head = dummyNode;
       // 一直逐个进行比较,直到一条链表为空
        while (list1 != null && list2 != null) {
            // 连接上较小值节点,并使其链表指针后移一个
            if (list1.val > list2.val) {
                head.next = list2;
                list2 = list2.next;
            } else {
                head.next = list1;
                list1 = list1.next;
            }
            // 让新链表指针后移,随时准备连接新节点
            head = head.next;
        }
      	// 至多一条链表为null,并且不为null的链表都为较大值,直接连接上即可
        head.next = list1 == null ? list2 : list1;
        // 虚拟节点仅用于链接,next才为真正有效的节点
        return dummyNode.next;
    }

(4) 双指针链表应用

(4.1) 链表的中间结点

力扣链接:链表的中间结点

在这里插入图片描述

我们可以暴力的求出单链表的长度,即可轻松找到位于中间的节点。

这个问题用经典的快慢指针也可以轻松搞定,用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。当链表长度为奇数时,快指针刚好移动到末尾节点,慢指针移动到中间节点;当链表长度为偶数时,快指针刚好移动到null节点,慢指针移动到中间的第二个节点节点;由于fast != null && fast.next != null条件,相当于链表始终为一个奇数链表进行移动。

   public ListNode middleNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        // 当快指针移动到末尾时,慢指针恰好移动到中间
        while (fast != null && fast.next != null) {
      		// 定义快指针走两格
            fast = fast.next.next;
            // 定义慢指针走一格
            slow = slow.next;
        }
        return slow;
    }

(4.2) 返回倒数第 k 个节点

力扣链接: 返回倒数第 k 个节点

在这里插入图片描述

与上一题类似,我们也可以使用快慢指针,不同的是我们需要先将fast 向后遍历到第k+1个节点, slow仍然指向链表的第一个节点,此时指针fast 与slow 二者之间刚好间隔 k 个节点。之后两个指针同步向后走,当 fast 走到链表的尾部空节点时,slow 指针刚好指向链表的倒数第k个节点。

    public int kthToLast(ListNode head, int k) {
        ListNode slow = head;
        ListNode fast = head;
        // 先令快指针移动k步
        while (k-- > 0) {
            fast = fast.next;
        }
        // 当快指针移动到null时,慢指针刚好移动到倒数第k个节点
        while (fast != null) {
            // 再令快慢指针同时移动
            fast = fast.next;
            slow = slow.next;
        }
        return slow.val;
    }

(4.3) 旋转链表

力扣链接: 旋转链表

在这里插入图片描述

仔细观察可以发现旋转链表其实就是将最后n个元素移动到了链表前面,因此我们可以利用与上一题寻找倒数第k个节点类似的方法,寻找到第k+1个节点(为什么不是第k个呢?因为我们需要从第k+1个开始链接操作)一波寻找过后,刚好快指针指向链表末节点,而慢指针指向第k+1个节点,此时就可以非常方便的开始链接操作了。

    public ListNode rotateRight(ListNode head, int k) {
        if (head == null || k == 0) {
            return null;
        }
        // 计算出链表的长度
        int num = 0;
        ListNode node = head;
        while (node != null) {
            num++;
            node = node.next;
        }
        // 定义快慢指针
        ListNode slow = head;
        ListNode fast = head;
        // 取模剔除无效循环
        k = k % num;
        while (k-- > 0) {
            fast = fast.next;
        }
        // 找到倒数第k+1个节点
        while (fast.next != null) {
            // 最后指向最后一个节点
            fast = fast.next;
            // 最后指向倒数第k+1个节点
            slow = slow.next;
        }
        // 开始进行链接操作
        // 令最后一个节点指向头节点
        fast.next = head;
        // 令头节点等于倒数第k个节点
        head = slow.next;
        // 令倒数第k+1个节点指向null
        slow.next = null;
        // 返回新的头节点
        return head;
    }

(4.4) 是否为环形链表

力扣链接: 环形链表

在这里插入图片描述

我们可以用HashSet依次遍历记录每个节点同时判断是否以及出现过,如果出现过就代表为环,否则不是环,借此轻松解决。我们也可以用双指针的思想来解决,类似于两个速度不一样的人在操场上跑步,在跑了N圈之后,速度快的人一定会超圈速度慢的人。

    public boolean hasCycle(ListNode head) {
        // 定义快慢指针
        ListNode slow = head, fast = head;
        // 如果为null说明是普通链表
        while (fast != null && fast.next != null) {
            // 如果不为null,快慢指针以不同速度行进
            slow = slow.next;
            fast = fast.next.next;
            // 两个指针相遇代表一定存在环
            if (slow == fast){
                return true;
            }
        }
        return false;
    }

(5) 删除链表元素

(5.1) 移除链表元素

力扣链接:移除链表元素

在这里插入图片描述

本题移除链表元素可能会碰到三种情况,即要删除的元素在链表的头部,中部,尾部,而删除头部与删除中部及尾部的处理方法不一致,如果我们单独处理将非常的繁琐,这时我们可以引入一个虚拟头节点指向头节点,这样我们是不是就可以剔除掉要删除的元素出现的头部的情况?接下来我们相当于只需要处理要删除的元素出现在链表的中部以及尾部,而这两种操作非常类似,我们仅需要找到要删除元素的前驱节点即可轻松完成。

    public ListNode removeElements(ListNode head, int val) {
        // 定义虚拟头节点指向头节点
        ListNode dummyNode = new ListNode();
        dummyNode.next = head;
         // 定义指针记录当前位置用于比较以及存储前驱节点
        ListNode cur = dummyNode.next, pre = dummyNode;
        while (cur != null) {
            // 如果当前位置为val,则通过前驱节点删除
            // 当前位置后移,前驱节点记录指针不移动
            if (cur.val == val) {
                pre.next = cur.next;
                cur = cur.next;
                continue;
            }
            // 如果当前位置不为val,当前位置以及前驱位置指针同时移动
            cur = cur.next;
            pre = pre.next;
        }
        // 由于虚拟头节点仅用于排除头节点情况,并不存储数据,因此我们需要返回其next
        return dummyNode.next;
    }

(5.2) 删除链表的倒数第 N 个结点

力扣链接: 删除链表的倒数第 N 个结点

在这里插入图片描述

这题与 返回倒数第 k 个节点 类似,不过我们要删除第k个节点则必须找到第k个节点的前驱节点才行,而移除元素可能会碰到三种情况,即要删除的元素在链表的头部,中部,尾部,而删除头部与删除中部及尾部的处理方法不一致,如果我们单独处理将非常的繁琐,这时我们同样可以引入一个虚拟头节点指向头节点,这样我们是不是就可以剔除掉要删除的元素出现的头部的情况?

   public ListNode removeNthFromEnd(ListNode head, int n) {
      	// 引入虚拟头节点使操作一致
        ListNode dummyNode = new ListNode();
        dummyNode.next = head;
        ListNode fast = dummyNode, slow = dummyNode;
        // 先让快指针走n步 
        while (n-- > 0) {
            fast = fast.next;
        }
       	// 找到第 k + 1 个节点
        while (fast.next != null) {
            slow = slow.next;
            fast = fast.next;
        }
        // 进行移除操作
        slow.next = slow.next.next;
        return dummyNode.next;
    }

(5.3) 删除排序链表中的重复元素

力扣链接: 删除排序链表中的重复元素

在这里插入图片描述

由于给定的链表是排好序的,因此可以推出重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。具体地,我们从指针 cur 指向链表的头节点,随后开始对链表进行遍历。如果当前 cur 与cur.next 对应的元素相同,那么我们就将cur.next 从链表中移除;否则说明链表中已经不存在其它与cur 对应的元素相同的节点,因此可以将 cur 指向 cur.next。当遍历完整个链表之后,我们返回链表的头节点即可。

    public ListNode deleteDuplicates(ListNode head) {
        // 为null直接返回
        if(head == null){
            return null;
        }
        ListNode temp = head;
        // 遍历到最后一个节点即可
        while (temp.next != null) {
            // 如果和下一位相等,则指向下下位,因为我们并不能确保当前元素和下下是否相等,
            // 还需要做一次判断,所以不能移动
            if (temp.val == temp.next.val) {
                temp.next = temp.next.next;
            } else {
                // 如果和下一位不相等,则移动到下一位
                temp = temp.next;
            }
        }
        return head;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1278446.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件工程 - 第8章 面向对象建模 - 4 - 物理体系结构建模

构件图 构件图概述 构件图描述了软件的各种构件和它们之间的依赖关系。 构件图的作用 在构件图中&#xff0c;系统中的每个物理构件都使用构件符号来表示&#xff0c;通常&#xff0c;构件图看起来像是构件图标的集合&#xff0c;这些图标代表系统中的物理部件&#xff0c;…

前端监控学习笔记

现成的SDK SentryFun Debug 需要监控什么&#xff1f; 错误统计 记录我们代码发布到线上各种奇奇怪怪的错误 行为日志埋点 记录用户行为&#xff0c;比如&#xff1a;分析用户浏览时间比较长的页面有哪些&#xff0c;常常点击的有哪些&#xff0c;可以做 相应的推荐 PV/UV统…

管理类联考-性质

性质 ——性质—— 一、是什么 &#xff08;1&#xff09;本质&#xff1a;判断一定范围内的对象是否具备某个性质的命题就是性质命题&#xff08;直言命题&#xff09;。直言命题是断定事物/对象是否具有某种性质的命题。直言命题在结构上由主项、谓项、联项和量项组成。 &am…

【ArcGIS Pro微课1000例】0039:制作全球任意经纬网的两种方式

本文讲解在ArcGIS Pro中制作全球任意经纬网的两种方式。 文章目录 一、生成全球经纬网矢量1. 新建地图加载数据2. 创建经纬网矢量数据二、布局生成经纬网1. 新建布局2. 创建地图框2. 创建经纬网一、生成全球经纬网矢量 以1:100万比例尺地图分幅为例,创建经差6、维差4的经纬网…

<软考>软件设计师-1计算机组成与结构(总结)

(一)计算机系统基础知识 1 计算机硬件组成 计算机的基本硬件系统由运算器、控制器、存储器、输入设备 和 输出设备 5大部件组成。 1 运算器、控制器等部件被集成在一起统称为中央处理单元(CPU) 。CPU是硬件系统的核心&#xff0c;用于数据的加工处理&#xff0c;能完成各种算…

gitlab高级功能之容器镜像仓库

今天给大家介绍一个gitlab的高级功能 - Container Registry&#xff0c;该功能可以实现docker镜像的仓库功能&#xff0c;将gitlab上的代码仓的代码通过docker构建后并推入到容器仓库中&#xff0c;好处就是无需再额外部署一套docker仓库。 文章目录 1. 参考文档2. Container R…

mybatis数据输入-Map类型参数输入

1、建库建表 CREATE DATABASE mybatis-example;USE mybatis-example;CREATE TABLE t_emp(emp_id INT AUTO_INCREMENT,emp_name CHAR(100),emp_salary DOUBLE(10,5),PRIMARY KEY(emp_id) );INSERT INTO t_emp(emp_name,emp_salary) VALUES("tom",200.33); INSERT INTO…

P1 嵌入式开发之什么是Linux应用开发

目录 前言 01 .Linux应用与裸机编程、驱动编程之间的区别 1.1裸机编程&#xff1a; 1.2 驱动编程 1.3应用编程 前言 &#x1f3ac; 个人主页&#xff1a;ChenPi &#x1f43b;推荐专栏1: 《C_ChenPi的博客-CSDN博客》✨✨✨ &#x1f525; 推荐专栏2: 《Linux C应用编程&a…

波奇学C++:智能指针(二):auto_ptr, unique_ptr, shared_ptr,weak_ptr

C98到C11&#xff1a;智能指针分为auto_ptr, unique_ptr, shared_ptr&#xff0c;weak_ptr,这几种智能都是为了解决指针拷贝构造和赋值的问题 auto_ptr&#xff1a;允许拷贝&#xff0c;但只保留一个指向空间的指针。 管理权转移&#xff0c;把拷贝对象的资源管理权转移给拷贝…

Centos7.9搭建zabbix6.4.0过程及报错注意点

搭建参考此链接即可&#xff1a;https://blog.csdn.net/PerDrix/article/details/129624091 报错整理&#xff1a; 一、zabbix6.0以上版本默认必须安装mysql 8.0.30以上版本数据库&#xff0c;否则服务起不来 二、编译安装zabbix时&#xff0c;必须执行如下操作&#xff0c;…

LeetCode算法题解(动态规划)|LeetCode1143. 最长公共子序列、LeetCode1035. 不相交的线、LeetCode53. 最大子数组和

一、LeetCode1143. 最长公共子序列 题目链接&#xff1a;1143. 最长公共子序列 题目描述&#xff1a; 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 &#xff0c;返回 0 。 一个字符串的 子序列 是指这样一…

四、设置主机名和域名映射

目录 1、配置每台虚拟机主机名 2、配置每台虚拟机域名映射 1、配置每台虚拟机主机名

Unity对接后台和加载图片

1、前言 在unity中与后台对接&#xff0c;用await在web端暂时还不支持&#xff0c;所以&#xff0c;协程成为比较好的通用方式&#xff0c;以下适用除post访问外的所有对接 2、对接后台 2.1、安装插件 首先我们需要用到Newtonsoft.dll&#xff0c;如果没有这个.dll的请跟着我…

vue权限管理解决方案

一. 什么是权限管理 权限控制是确保用户只能访问其被授权的资源和执行其被授权的操作的重要方面。而前端权限归根结底是请求的发起权&#xff0c;请求的发起可能有下面两种形式触发 页面加载触发页面上的按钮点击触发 总体而言&#xff0c;权限控制可以从前端路由和视图两个…

QProcess 启动 进程 传参数 启动控制台进程 传参

目录 QProcess 启动外部程序的两种方式 依赖式 分离式&#xff1a; 启动进程前的预处理 设置启动路径 设置启动命令参数 设置启动工作目录 设置启动所需环境&#xff1a; 启动的状态 code smple: QProcess 控制台进程 QProcess启动控制台不显示窗口 注意&#xff1a;…

jvm基本概念,运行的原理,架构图

文章目录 JVM(1) 基本概念:&#xff08;2&#xff09;运行过程 今天来和大家聊聊jvm&#xff0c; JVM (1) 基本概念: JVM 是可运行Java代码的假想计算机&#xff0c;包括一套字节码指令集、一组寄存器、一个栈一个垃圾回收&#xff0c;堆 和 一个存储方法域。JVM 是运行在操作…

9.ROS的TF坐标变换(三):坐标系关系查看与一个案例

1 查看目前的坐标系变化 我们先安装功能包&#xff1a; sudo apt install ros-melodic-tf2-tools安装成功&#xff01; 我们先启动上次的发布坐标变换的节点&#xff1a; liuhongweiliuhongwei-Legion-Y9000P-IRX8H:~/Desktop/final/my_catkin$ source devel/setup.bash liuho…

cyclictest 交叉编译与使用

目录 使用版本问题编译 numactl编译 cyclictest使用参考 cyclictest 主要是用于测试系统延时&#xff0c;进而判断系统的实时性 使用版本 rt-tests-2.6.tar.gz numactl v2.0.16 问题 编译时&#xff0c;需要先编译 numactl &#xff0c;不然会有以下报错&#xff1a; arm-…

Linux:优化原则

web系统的优化原则&#xff1a; 从单机到集群 对Linux系统自身的优化原则&#xff1a;

TCP报文解析

1.端口号 标记同一台计算机上的不同进程 源端口&#xff1a;占2个字节&#xff0c;源端口和IP的作用是标记报文的返回地址。 目的端口&#xff1a;占2个字节&#xff0c;指明接收方计算机上的应用程序接口。 TCP报头中的源端口号和目的端口号同IP报头中的源IP和目的IP唯一确定一…