一篇解决链表(List)详解(三)

文章目录

1、单链表定义

采用的是链式存储结构,使用一组地址任意的存储单元来存放数据元素。在单链表中,存储的每一条数据都是以节点来表示的,每个节点的构成都是:元素(存储数据的存储单元)+指针(存储下一个节点的地址值),如下图所示:
在这里插入图片描述
首节点:单链表的开始节点
尾节点:单链表的终端节点
在这里插入图片描述

1.1、根据序号获取节点的操作(时间复杂度O(n))

在线性表中,每个节点都有一个唯一的序号,该序号是从0开始递增。通过序号获取单链表的节点时,我们需要从链表的首节点开始,从前往后开始循环遍历,直到遇到查询序号所对应的节点时为止。
以下图为例,我们需要获得序号为 2 的节点,那么就需要依次遍历获得“节点 1”和“节点 2”,然后才能获得序号为 2 的节点,也就是“节点 3”。
在这里插入图片描述

1.2、根据序号删除节点的操作(时间复杂度O(1))

首先根据序号获得需要删除的节点,然后让"删除的节点的前一个节点"指向"删除节点的后一个节点",这就实现了节点的删除操作。
以下图为例,我们需要删除序号为 2 的节点,那么就让“节点 2”指向“节点 4”即可,这样就删除了序号为 2 的节点,也就是删除了“节点 3”。
在这里插入图片描述

1.3、根据序号插入节点的操作(时间复杂度O(1))

根据序号插入节点的操作,我们首先应该根据序号找到插入的节点位置,然后让“插入位置的
上一个节点”指向“新插入的节点”,然后再让“新插入的节点”指向“插入位置的节点”,这样就实现了节点的插入操作。
以下图为例,我们需要在序号为 2 的位置插入元素值“0”,首先先把字符串“0”封装为一
个节点对象,然后就让“节点 2”指向“新节点 0”,最后再让“节点 0”指向“节点 3”,这样就插入了一个新节点。
在这里插入图片描述

1.4、顺序表(数组)和单链表(链表)的比较

(1) 存储方式比较
顺序表采用一组地址连续的存储单元依次存放数据元素,通过元素之间的先后顺序来确定元素
之间的位置,可以借助CPU的缓存机制,预读数组中的数据,因此访问效率更高,存储空间的利用率较高。
单链表采用一组地址任意的存储单元来存放数据元素,通过存储下一个节点的地址值来确定节
点之间的位置,对CPU缓存不友好,没办法有效预读,因此访问效率低,存储空间的利用率较低。
(2) 时间性能比较
顺序表查找的时间复杂度为 O(1),插入和删除需要移动元素,因此时间复杂度为 O(n)。若是需要频繁的执行查找操作,但是很少进行插入和删除操作,那么建议使用顺序表。
单链表查找的时间复杂度为 O(n),插入和删除无需移动元素,因此时间复杂度为 O(1)。若是需要频繁的执行插入和删除操作,但是很少进行查找操作,那么建议使用链表。
补充:根据序号来插入和删除节点,需要通过序号来找到插入和删除节点的位置,那么整体的
时间复杂度为 O(n)。因此,单链表适合数据量较小时的插入和删除操作,如果存储的数据量较大,那么就建议使用别的数据结构,例如使用二叉树来实现。
(3) 空间性能比较
顺序表需要预先分配一定长度的存储空间,如果事先不知道需要存储元素的个数,分配空间过
大就会造成存储空间的浪费,分配空间过小则需要执行耗时的扩容操作。
单链表不需要固定长度的存储空间,可根据需求来进行临时分配,只要有内存足够就可以分配,在链表中存储元素的个数是没有限制的,无需考虑扩容操作。

1.5单链表代码实现

/**
 * @author :zhz
 * @date :Created in 2020/12/22
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: node节点类
 **/
public class ListNode {
    int value;//值
    ListNode next;//下一个指针

    public ListNode(int value) {
        this.value = value;
        this.next=null;
    }
}

/**
 * @author :zhz
 * @date :Created in 2020/12/22
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: 单链表实现
 **/
public class MyLinkedList {
    private ListNode head;

    //插入链表的头部   data就是插入的数据
    public void insertHead(int data){
        //不管当前是否有数据,只需要把新节点的指针,指向已有的head,再把head指向新节点(栈内存的引用)
        ListNode newNode=new ListNode(data);
        newNode.next=head;
        head=newNode;
    }

    //插入链表的中间 假设定义在第N个插入 O(n)
    public void insertNth(int data,int position){
        if (position==0){
            insertHead(data);//头插入
        }else {
            ListNode cur=head;
            for (int i = 0; i < position; i++) {
                cur=cur.next;//向后遍历到插入元素的前一个节点
            }
            ListNode newNode=new ListNode(data);//创建新节点
            //插入操作
            newNode.next=cur.next;
            cur.next=newNode;
        }
    }
    //删除头结点
    public void deleteHead(){//O(1)
        head=head.next;
    }
    //删除链表的中间
    public void deleteNth(int position) {//O(n)
        if (position==0){
            deleteHead();
        }else{
            ListNode cur=head;
            for (int i = 0; i < position; i++) {
                cur=cur.next;//向后遍历到插入元素的前一个节点
            }
            //删除操作
            cur.next=cur.next.next;//cur.next 表示的是删除的点,后一个next就是我们要指向的
        }
    }
    //找节点
    public void find(int data) {//O(n)
        ListNode cur=head;
        while (cur!=null){
            if(cur.value==data){
                break;
            }
            cur=cur.next;
        }
    }
    //打印节点
    public void print(){
        ListNode cur=head;
        while (cur!=null){
            System.out.print(cur.value+" ");
            cur=cur.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        MyLinkedList myList = new MyLinkedList();
        myList.insertHead(5);
        myList.insertHead(7);
        myList.insertHead(10);
        myList.print(); // 10 -> 7 -> 5
//        myList.deleteNth(0);
//        myList.print(); // 7 -> 5
//        myList.deleteHead();
//        myList.print(); // 5
//        myList.insertNth(11, 1);
//        myList.print(); // 5 -> 11
//        myList.deleteNth(1);
//        myList.print(); // 5
    }
}

2、双链表定义

也叫双向链表,采用的是链式储存处结构。在双链表中,每个节点中都有两个指针,分别指向直接前驱节点(保存前一个节点的地址值)和直接后继节点(保存后一个节点的地址值),如下图所示
在这里插入图片描述

2.1、特点

双链表的任意一个节点,都可以很方便的访问它的直接前驱节点和直接后驱节点,如下图所示
在这里插入图片描述

2.2、单链表和双链表的区别

逻辑上无区别,他们均是完成线性表的内容,主要的区别是结构上的构造有所区别
1、单链表
对于一个节点,有储存数据的 data 和指向下一个节点的 next。也就是说,单链表的遍历操作都得通过前节点—>后节点。
2、 双链表
对于一个节点,有储存数据的 data 和指向下一个节点的 next,还有一个指向前一个节点的 pre。也就是说,双链表不但可以通过前节点—>后节点,还可以通过后节点—>前节点。

2.3、双链表代码实现

/**
 * @author :zhz
 * @date :Created in 2020/12/22
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: 双链表节点类
 **/
public class DNode {
    int value;      //值
    DNode next;     //下一个的指针
    DNode pre;      //指向的是前一个指针

    DNode(int value){
        this.value = value;
        this.next = null;
        this.pre = null;
    }
}

/**
 * @author :zhz
 * @date :Created in 2020/12/22
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: 双链表实现
 **/
public class DoubleLinkList {  // 双向链表

    private DNode head;        //头
    private DNode tail;        //尾

    public DoubleLinkList() {
        head=null;
        tail=null;
    }
    public void insertHead(int data){//插入头部
        DNode newNode=new DNode(data);
        if (head==null){
            tail=newNode;
        }else{
            head.pre=newNode;
            newNode.next=head;
        }
        head=newNode;
    }
    public void deleteHead(){//删除头部
        if (head==null){//没有数据
            return;
        }
        if(head.next==null){//表示只有头节点
            tail=null;
        }else{
            head.next.pre=null;
        }
        head=head.next;//把第二个节点置为当前头节点
    }
    public void deleteKey(int data){//删除指定的数据
        DNode current=head;
        while (current.value!=data){
            if (current.next==null){
                System.out.println("无节点");
                return;
            }
            current=current.next;//向后移一位
        }
        if (current==head){//表示删除头节点
            deleteHead();
        }else{
            current.pre.next=current.next;
            if (current==tail){//删除尾部
                tail=current.pre;
                current.pre=null;
            }else{
                current.next.pre=current.pre;
            }
        }
    }
}

3、环形链表定义

环形链表依旧采用的是链式存储结构,它的特点就是设置首节点和尾节点相互指向,从而实现
让整个链表形成一个环。常见的环形链表有:
(1) 环形单链表
在单链表中,尾节点的指针指向了首节点,从而整个链表形成一个环,如下图所示:
在这里插入图片描述
(2) 环形双链表
在双链表中,尾节点的指针指向了首节点,首节点的指针指向了尾节点,从而整个链表形成一
个环,如下图所示:
在这里插入图片描述

3.1、环形链表代码实现

/**
 * @author :zhz
 * @date :Created in 2020/12/22
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: 环形单链表的实现
 **/
public class CycleSingleLinkedList {
    /**
     * 用于保存单链表中的首节点
     */
    private CycleNode headNode;

    /**
     * 用于保存单链表中的尾节点
     */
    private CycleNode lastNode;

    /**
     * 用于保存单链表中节点的个数
     */
    private int size;

    /**
     * 添加元素
     *
     * @param element 需要添加的数据
     */
    public void add(Object element) {
        // 1.把需要添加的数据封装成节点对象
        CycleNode node = new CycleNode(element);
        // 2.处理单链表为空表的情况
        if (headNode == null) {
            // 2.1 把 node 节点设置为单链表的首节点
            headNode = node;
            // 2.2 把 node 节点设置为单链表的尾节点
            lastNode = node;
        } else {// 3.处理单链表不是空表的情况
            // 3.1 让 lastNode 指向 node 节点
            lastNode.next = node;
            // 3.2 更新 lastNode 的值
            lastNode = node;
        }
        // 4.设置 lastNode 的 next 值为 headNode
        lastNode.next = headNode;
        // 5.更新 size 的值
        size++;
    }

    /**
     * 根据序号获取元素
     *
     * @param index 序号
     * @return 序号所对应节点的数据值
     */
    public Object get(int index) {
        // 1.如果序号的取值小于 0,则证明是不合法的情况
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("序号不合法,index:" + index);
        }
        // 2.根据序号获得对应的节点对象
        CycleNode node = node(index);
        // 3.获取并返回 node 节点的数据值
        return node.data;
    }


    /**
     * 根据序号删除元素
     *
     * @param index 序号
     */
    public void remove(int index) {
        // 1.判断序号是否合法,合法取值范围:[0, size - 1]
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("序号不合法,index:" + index);
        }
        // 2.处理删除节点在开头的情况
        if (index == 0) {
            // 2.1 获得删除节点的后一个节点
            CycleNode nodeNext = headNode.next;
            // 2.2 设置 headNode 的 next 值为 null
            headNode.next = null;
            // 2.3 设置 nextNode 为单链表的首节点
            headNode = nodeNext;
            // 2.4 设置 lastNode 的 next 值为 headNode
            lastNode.next = headNode;
        } else if (index == size - 1) {// 3.处理删除节点在末尾的情况
            // 3.1 获得删除节点的前一个节点
            CycleNode preNode = node(index - 1);
            // 3.2 设置 preNode 的 next 值为 null
            preNode.next = null;
            // 3.3 设置 preNode 为单链表的尾节点
            lastNode = preNode;
            // 3.4 设置 lastNode 的 next 值为 headNode
            lastNode.next = headNode;
        } else {   // 4.处理删除节点在中间的情况
            // 4.1 获得 index-1 所对应的节点对象
            CycleNode preNode = node(index - 1);
            // 4.2 获得 index+1 所对应的节点对象
            CycleNode nodeNext = preNode.next.next;//或者 CycleNode nodeNext= node(index+1);
            // 4.3 获得删除节点并设置 next 值为 null
            preNode.next.next = null;//促进gc
            // 4.4 设置 preNode 的 next 值为 nextNode
            preNode.next = nodeNext;
        }
        // 5.更新 size 的值
        size--;
        // 6.判断 size 的值是否为 0,如果 size 的值为 0,则设置 headNode 和 lastNode 为null
        if (size == 0) {
            headNode = null;
            lastNode = null;
        }
    }

    /**
     * 根据序号插入元素
     *
     * @param index   序号
     * @param element 需要插入的数据
     */
    public void add(int index, Object element) {
        // 1.判断序号是否合法,合法取值范围:[0, size]
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("序号不合法,index:" + index);
        }
        // 2.把需要添加的数据封装成节点对象
        CycleNode curNode = new CycleNode(element);

        if (index==0){// 3.处理插入节点在开头位置的情况
            // 3.1 设置 node 的 next 值为 headNode
           curNode.next=headNode;
            // 3.2 设置 node 节点为单链表的首节点
            headNode= curNode;
            // 3.3 设置 lastNode 的 next 值为 headNode
            lastNode.next=headNode;
        }else if (index==size-1){// 4.处理插入节点在末尾位置的情况
            // 4.1 设置 lastNode 的 next 值为 node
            lastNode.next=curNode;
            // 4.2 设置 node 节点为单链表的尾节点
            lastNode=curNode;
            // 4.3 设置 lastNode 的 next 值为 headNode
            lastNode.next=headNode;
        }else{// 5.处理插入节点在中间位置的情况
            // 5.1 获得 index-1 所对应的节点对象
            CycleNode preNode = node(index - 1);
            // 5.2 获得 index 所对应的节点对象
            CycleNode cNode = preNode.next;
            // 5.3 设置 preNode 的 next 为 node
            preNode.next=curNode;
            // 5.4 设置 node 的 next 为 curNode
                curNode.next=cNode;
        }
        // 6.更新 size 的值
        size++;
}

    /**
     * 根据序号获得对应的节点对象
     *
     * @param index 序号
     * @return 序号对应的节点对象
     */
    private CycleNode node(int index) {
        // 1.判断环形单链表是否为空表
        if (headNode == null) {
            throw new NullPointerException("环形单链表为空表");
        }
        // 2.定义一个零时节点,用于辅助单链表的遍历操作
        CycleNode tempNode = headNode;
        // 3.定义一个循环,用于获取 index 对应的节点对象
        for (int i = 0; i < index % size; i++) {
            // 4.更新 tempNode 的值
            tempNode = tempNode.next;
        }
        // 5.返回 index 对应的节点对象
        return tempNode;
    }
}

4、经典面试题

4.1、如何设计一个LRU缓存淘汰算法?

//实现一:利用LinkedHashMap
import sun.misc.LRUCache;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;

/**
 * @author zhz
 * @date 2020/06/03
 * 运用你所掌握的数据结构,设计和实现一个  LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
 *
 * 获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。
 * 写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。
 * 当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
 **/
public class _146_LRU缓存机制 extends LinkedHashMap<Integer, Integer> {

    private int capacity;

    /**
     *  移除最近最少被访问条件之一,通过覆盖此方法可实现不同策略的缓存
     *  LinkedHashMap是默认返回false的,我们可以继承LinkedHashMap然后复写该方法即可
     *  例如 LeetCode 第 146 题就是采用该种方法,直接 return size() > capacity;
     * @param eldest
     * @return
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size()>capacity;
    }

    public _146_LRU缓存机制(int capacity) {
        super(capacity,0.75F,true);
        this.capacity=capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key,value);
    }

    public static void main(String[] args) {
        _146_LRU缓存机制 cache = new _146_LRU缓存机制( 2);

        cache.put(1, 1);
        cache.put(2, 2);
        System.out.println(cache.get(1));
        cache.put(3, 3);    // 该操作会使得关键字 2 作废
        System.out.println(cache.get(2));
        cache.put(4, 4);    // 该操作会使得关键字 1 作废
        System.out.println(cache.get(1));
        System.out.println(cache.get(3));
        System.out.println(cache.get(4));
    }
}
//实现方式二,维护一个有序的单链表,而有序就是加入的时间排序

4.2、约瑟夫问题

描述:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。
现在问你最后留下的人是谁?
比如N=6,M=5
留下的就是1
1 2 3 4 5 6 => 6 1 2 3 4 => 6 1 2 3 =>1 2 3 => 1 3 => 1

/**
 * @author :zhz
 * @date :Created in 2020/12/23
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: 环形单链表的约瑟夫问题
 **/
public class Josephus {
    public static void main(String[] args) {
        // 创建一个环形单链表
        Node lastNode = new Node(10);
        Node node9 = new Node(9, lastNode);
        Node node8 = new Node(8, node9);
        Node node7 = new Node(7, node8);
        Node node6 = new Node(6, node7);
        Node node5 = new Node(5, node6);
        Node node4 = new Node(4, node5);
        Node node3 = new Node(3, node4);
        Node node2 = new Node(2, node3);
        Node headNode = new Node(1, node2);
        lastNode.next = headNode;
        // 执行约瑟夫方法
        josephus(headNode, lastNode, 10, 2, 3);
    }

    /**
     * 约瑟夫问题
     *
     * @param headNode 环形单链表的首节点
     * @param lastNode 环形单链表的尾节点
     * @param size     环形单链表中节点的个数
     * @param start    从编号为 start 的小孩开始报数
     * @param count    每次数几下
     */
    private static void josephus(Node headNode, Node lastNode, int size, int start, int count) {
        // 1.处理不合法的情况
        // 1.1 处理 headNode 为 null 的情况
        if (headNode == null) {
            throw new NullPointerException("headNode 为 null");
        }
        // 1.2 处理 start 和 count 不合法的情况
        if (start > size || count > size) {
            throw new IllegalArgumentException("参数不合法");
        }
        // 2.设置编号为 start 的小孩开始报数,并且使用 headNode 指向该节点
        for (int i = 0; i < start - 1; i++) {// 把 headNode 和 lastNode 往后移动 start-1 次
            headNode = headNode.next;
            lastNode = lastNode.next;
        }
        // 3.定义一个循环,用于循环的执行报数操作
        while (size != 0) { // 如果 size 等于 0,则停止报数
            // 4.执行报数操作,也就是找到需要出圈的小孩,我们使用 headNode 指向需要出圈的节点
            // 把 headNode 和 lastNode 往后移动 count-1 次
            for (int i = 0; i < count - 1; i++) {
                headNode = headNode.next;
                lastNode = lastNode.next;
            }
            // 5.输出需要出圈小孩的编号,也就是输出 headNode 保存的数据值
            System.out.println(headNode.data);
            // 6.实现小孩的出圈操作,也就是把 headNode 从环形单链表中删除
            // 6.1 获得删除节点的后一个节点
            Node nextNode = headNode.next;
            // 6.2 设置 lastNode 的 next 值为 nextNode
            lastNode.next = nextNode;
            // 6.3 设置 headNode 的 next 值为 null
            headNode.next = null;//促进gc
            // 7.更新 size 的值
            size--;
            // 8.设置 headNode 指向 nextNode
            headNode = nextNode;
        }
    }
    /**
     * 节点类
     */
    private static class Node {
        /**
         * 用于保存节点中的数据值
         */
        private Object data;
        /**
         * 用于保存下一个节点的地址值
         */
        private Node next;

        /**
         * 专门为 data 做初始化的工作
         *
         * @param data
         */
        public Node(Object data) {
            this.data = data;
        }

        /**
         * 专门为 data 和 next 做初始化的工作
         *
         * @param data
         * @param next
         */
        public Node(Object data, Node next) {
            this.data = data;
            this.next = next;
        }
    }

}

4.3、单链表的反转(反转链表)(https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof)

/**
 * @author zhz
 * @date 2020/04/29
 * 反转链表
 **/
public class ReverseList {
    /**
     *解题思路:递归(从下往上看),假设我们 输入: 1->2->3->4->5->NULL,然后我们从5开始看,
     *我们创建一个新的临时链表来存储返回的数据new_list,当new_list=5,输出new_list=5,因为head的下
     *一个的为null了,然后我们继续递归new_list=4,
     *
     *
     */
    public ListNode reverseList(ListNode head) {
        //递归
        if(head==null || head.next==null){
            return head;
        }
        ListNode new_list=reverseList(head.next);//除了头结点外的其他都反转1->2<-3<-4<-5
        head.next.next=head;
        head.next=null;
        return new_list;
    }
    //三指针迭代遍历
    public ListNode reverseList1(ListNode head) { 
        if(head==null){
            return head;
        }
        ListNode tail=null;
        ListNode first=head;
        ListNode second=head.next;
        while(first!=null){
            first.next=tail;//头节点的下一个节点是尾节点
            tail=first;   //尾节点变为头节点
            first=second;//让原先的第二个节点赋值给头节点
            if(second!=null){
                second=second.next;
            }
        }
        return tail;
    }
        /**
     * 迭代
     * @param head
     * @return
     */
    public ListNode reverseLinkedList(ListNode head){
        ListNode cur = head, pre = null;
        while(cur != null) {
            ListNode tmp = cur.next; // 暂存后继节点 cur.next
            cur.next = pre;          // 修改 next 引用指向
            pre = cur;               // pre 暂存 cur
            cur = tmp;               // cur 访问下一节点
        }
        return pre;
    }
}

4.4、查找链表的中间节点(https://leetcode-cn.com/problems/middle-of-the-linked-list/)

/**
 * @author :zhz
 * @date :Created in 2020/12/23
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: 链表的中间节点
 **/
public class MiddleOfTheLinkedList {
    //第一种:数组法(时间复杂度O(n),空间复杂度O(n))
    public ListNode middleNode(ListNode head) {
        ListNode[] nodes = new ListNode[100];
        int index = 0;
        while (head != null) {
            nodes[index++] = head;
            head = head.next;
        }
        return nodes[index / 2];
    }

    //快慢指针
    public ListNode middleNode1(ListNode head) {
        ListNode first = head;
        ListNode last = head;
        while (last != null && last.next != null) {
            first = first.next;
            last = last.next.next;
        }
        return first;
    }

}

4.5、在O(1)时间删除单链表节点(https://leetcode-cn.com/problems/shan-chu-lian-biao-de-jie-dian-lcof)

/**
 * @author :zhz
 * @date :Created in 2020/12/24
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: 在O(1)时间删除单链表节点
 **/
public class DeleteNode {
    public ListNode deleteNode(ListNode head, int val) {
        if(head.value==val){
            return head.next;
        }
        ListNode pre=head;
        ListNode cur=head.next;
        while (cur!=null&&cur.value!=val){
            pre=cur;
            cur=cur.next;
        }
        if (cur!=null){
            pre.next=cur.next;
        }
        return head;
    }
}

4.6、查找单链表倒数第k个节点(https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof)

package com.hr.剑指offer.work1;

/**
 * @author zhz
 * @date 2020/06/16
 **/
public class 面试题22_链表中倒数第k个节点 {    /**
     * 双指针
     * 初始化: 前指针 former 、后指针 latter ,双指针都指向头节点 head​ 。
     * 构建双指针距离: 前指针 former 先向前走 kk 步(结束后,
     * 双指针 former 和 latter 间相距 kk 步)。
     * 双指针共同移动: 循环中,双指针 former 和 latter 每轮都向前走一步,
     * 直至 former 走过链表 尾节点 时跳出(跳出后, latter 与尾节点距离为 k-1,即 latter 指向倒数第 kk 个节点)。
     * 返回值: 返回 latter 即可。
     */
    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode first=head;
        ListNode last=head;
        for (int i = 0; i < k; i++) {
            first=first.next;
        }
        while (first!=null){
            first=first.next;
            last=last.next;
        }
        return last;
    }
}

4.7、合并两个有序的链表(https://leetcode-cn.com/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof)

package com.hr.剑指offer.work1;

/**
 * @author zhz
 * @date 2020/06/17
 * 1、初始化: 伪头节点 dum ,节点 cur 指向 dum 。
 * 2、循环合并: 当 l1或 l2为空时跳出;
 *      1、当 l1.val<l2.val 时: cur 的后继节点指定为 l1,并 l1向前走一步;
 *      2、当 l1.val≥l2.val 时: cur 的后继节点指定为 l2,并 l2向前走一步 ;
 *      3、节点 cur 向前走一步,即 cur = cur.nextcur=cur.next 。
 * 3、合并剩余尾部: 跳出时有两种情况,即 l1为空 或 l\2为空。
 *      1、若 l1 != null : 将 l1添加至节点 cur 之后;
 *      2、否则: 将 l2添加至节点 cur 之后。
 * 4、返回值: 合并链表在伪头节点 dumdum 之后,因此返回 dum.next即可。
 **/
public class 面试题25_合并两个排序的链表 {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dum=new ListNode(0);
        ListNode cur=dum;
        while (l1!=null&&l2!=null){
            if (l1.val<l2.val){
                cur.next=l1;
                l1= l1.next;
            }else{
                cur.next=l2;
                l2= l2.next;
            }
            cur=cur.next;
        }
        cur.next=l1!=null?l1:l2;
        return dum.next;
    }
}

4.8、从尾到头打印单链表(https://leetcode-cn.com/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof)

package com.hr;

import java.util.Stack;

/**
 * 从尾到头打印单链表(使用栈来实现)
 */
public class Test06 {
    public static void main(String[] args) {
        // 1.创建一个单链表
        Node lastNode = new Node(44);
        Node node3 = new Node(33, lastNode);
        Node node2 = new Node(22, node3);
        Node headNode = new Node(11, node2);
        // 2.从尾到头打印单链表
        reversePrint(headNode);
    }

    /**
     * 从尾到头打印单链表
     *利用栈实现
     * @param headNode 单链表的首节点
     */
    public static void reversePrint(Node headNode) {
        // 1.处理 headNode 为 null 的情况
        if (headNode == null) {
            throw new NullPointerException("headNode 为 null");
        }
        // 2.创建一个栈
        Stack<Node> stack = new Stack<>();
        // 3.遍历单链表中的所有节点
        Node tempNode = headNode;
        while (tempNode != null) {
            // 4.把遍历出来的节点添加进入栈中
            stack.push(tempNode);
            tempNode = tempNode.getNext();
        }
        // 5.遍历栈中的所有节点
        while (!stack.isEmpty()) {
            Node node = stack.pop();
            System.out.println(node.getData());
        }
    }
    /**
     * 从尾到头打印单链表(使用递归来实现)
     * @param headNode 单链表的首节点
     */
    public static void reversePrint(Node headNode) {
        // 1.判断 headNode 为 null 的情况
        if (headNode == null) {
            return;
        }
        // 2.从尾到头打印以 headNode 下一个节点为首节点的链表
        reversePrint(headNode.getNext());
        // 3.打印输出 headNode 中的数据值
        System.out.println(headNode.getData());
    }
}

4.9、判断单链表是否有环(https://leetcode-cn.com/problems/linked-list-cycle/)

package com.hr;

/**
 * 判断单链表是否有环
 */
public class Test08 {
    public static void main(String[] args) {
        // 1.创建一个单链表
        Node lastNode = new Node(55);
        Node node4 = new Node(44, lastNode);
        Node node3 = new Node(33, node4);
        Node node2 = new Node(22, node3);
        Node headNode = new Node(11, node2);
        lastNode.setNext(node2);
        // 2.判断单链表是否有环
        boolean flag = hasCycle(headNode);
        System.out.println(flag);
    }

    /**
     * 判断单链表是否有环
     *
     * @param headNode 单链表的首节点
     * @return 如果单链表有环,则返回 true,否则返回 fasle
     */
    public static boolean hasCycle(Node headNode) {
        // 1.处理 headNode 为 null 的情况
        if (headNode == null) {
            return false;
        }
        // 2.定义一个快指针,每次往后走两步
        Node fast = headNode;
        // 3.定义一个慢指针,每次往后走一步
        Node slow = headNode;
        // 4.定义一个循环,用于判断单链表是否有环
        while (fast != null && fast.getNext() != null) {
            // 5.设置快慢指针每次往后移动
            fast = fast.getNext().getNext();
            slow = slow.getNext();
            // 6.如果 fast 和 slow 指向的是同一个节点,则证明单链表有环
            if (fast == slow) {
                return true;
            }
        }
        // 7.执行到此处,证明单链表是无环单链表
        return false;
    }
}

4.10、从有环链表中,获得环的长度

package com.hr;

/**
 * 从有环链表中,获得环的长度
 */
public class Test09 {

    public static void main(String[] args) {
        // 1.创建一个单链表
        Node lastNode = new Node(66);
        Node node5 = new Node(55, lastNode);
        Node node4 = new Node(44, node5);
        Node node3 = new Node(33, node4);
        Node node2 = new Node(22, node3);
        Node headNode = new Node(11, node2);
        lastNode.setNext(node2);
        // 2.从有环链表中,获得环的长度
        int size = getCycleLength(headNode);
        System.out.println(size);
    }

    /**
     * 从有环链表中,获得环的长度
     *
     * @param headNode 单链表的首节点
     * @return 如果单链表有环,则返回环中节点的个数,否则返回 0
     */
    public static int getCycleLength(Node headNode) {
        // 1.获得快慢指针相交的节点
        Node meetNode = meetNode(headNode);
        // 2.处理 meetNode 为 null 的情况
        if (meetNode == null) {
            return 0;
        }
        // 3.定义一个变量,用于保存环中节点的个数
        int size = 0;
        // 4.从 meetNode 节点开始,遍历环中的节点
        // 4.1 定义一个零时节点,用于辅助单链表的遍历
        Node tempNode = meetNode;
        // 4.2 定义一个死循环,用于遍历环中的所有节点
        while (true) {
            // 4.3 让 tempNode 指向它的下一个节点
            tempNode = tempNode.getNext();
            // 4.4 更新 size 的值
            size++;
            // 5.如果 tempNode 和 meetNode 指向的是同一个节点,那么就需要停止遍历操作
            if (tempNode == meetNode) {
                break;
            }
        }
        // 6.返回环中节点的个数
        return size;
    }

    /**
     * 获得快慢指针相交的节点
     *
     * @param headNode 单链表的首节点
     * @return 如果单链表有环,则返回快慢指针相交的节点,否则返回 null。
     */
    public static Node meetNode(Node headNode) {
        // 1.处理 headNode 为 null 的情况
        if (headNode == null) {
            return null;
        }
        // 2.定义一个快指针,每次往后走两步
        Node fast = headNode;
        // 3.定义一个慢指针,每次往后走一步
        Node slow = headNode;
        // 4.定义一个循环,用于判断单链表是否有环
        while (fast != null && fast.getNext() != null) {
            // 5.设置快慢指针每次往后移动
            fast = fast.getNext().getNext();
            slow = slow.getNext();
            // 6.如果 fast 和 slow 指向的是同一个节点,则证明单链表有环
            if (fast == slow) {
                return fast;
            }
        }
        // 7.执行到此处,证明单链表是无环单链表
        return null;
    }
}

4.11、单链表中,取出环的起始点(https://leetcode-cn.com/problems/linked-list-cycle-ii/)

package com.hr;

/**
 * 单链表中,取出环的起始点
 */
public class Test10 {
    public static void main(String[] args) {
        // 1.创建一个单链表
        Node lastNode = new Node(66);
        Node node5 = new Node(55, lastNode);
        Node node4 = new Node(44, node5);
        Node node3 = new Node(33, node4);
        Node node2 = new Node(22, node3);
        Node headNode = new Node(11, node2);
        lastNode.setNext(node2);
        // 2.单链表中,取出环的起始点
        Node startNode = getStartNode(headNode);
        System.out.println(startNode.getData());
    }

    /**
     * 单链表中,取出环的起始点
     *
     * @param headNode 单链表中的首节点
     * @return 返回带环单链表中环的起始点
     */
    public static Node getStartNode(Node headNode) {
        // 1.获得带环单链表中环的长度
        int length = getCycleLength(headNode);
        // 2.处理 length 为 0 的情况,也就是处理单链表没有环的情况
        if (length == 0) {
            return null;
        }
        // 3.定义 first 和 second 指针,并且设置初始值为单链表的首节点
        Node first = headNode, second = headNode;
        // 4.让 first 指针往后移动 length 次
        for (int i = 0; i < length; i++) {
            first = first.getNext();
        }
        // 5.定义一个循环,用于获得带环单链表中环的起始点
        while (first != second) {
            // 6.设置 first 和 second 每次往后移动一步
            first = first.getNext();
            second = second.getNext();
        }
        // 6.返回带环单链表中环的起始点
        return first;
    }

    /**
     * 从有环链表中,获得环的长度
     *
     * @param headNode 单链表的首节点
     * @return 如果单链表有环,则返回环中节点的个数,否则返回 0
     */
    public static int getCycleLength(Node headNode) {
        // 1.获得快慢指针相交的节点
        Node meetNode = meetNode(headNode);
        // 2.处理 meetNode 为 null 的情况
        if (meetNode == null) {
            return 0;
        }
        // 3.定义一个变量,用于保存环中节点的个数
        int size = 0;
        // 4.从 meetNode 节点开始,遍历环中的节点
        // 4.1 定义一个零时节点,用于辅助单链表的遍历
        Node tempNode = meetNode;
        // 4.2 定义一个死循环,用于遍历环中的所有节点
        while (true) {
            // 4.3 让 tempNode 指向它的下一个节点
            tempNode = tempNode.getNext();
            // 4.4 更新 size 的值
            size++;
            // 5.如果 tempNode 和 meetNode 指向的是同一个节点,那么就需要停止遍历操作
            if (tempNode == meetNode) {
                break;
            }
        }
        // 6.返回环中节点的个数
        return size;
    }

    /**
     * 获得快慢指针相交的节点
     *
     * @param headNode 单链表的首节点
     * @return 如果单链表有环,则返回快慢指针相交的节点,否则返回 null。
     */
    public static Node meetNode(Node headNode) {
        // 1.处理 headNode 为 null 的情况
        if (headNode == null) {
            return null;
        }
        // 2.定义一个快指针,每次往后走两步
        Node fast = headNode;
        // 3.定义一个慢指针,每次往后走一步
        Node slow = headNode;
        // 4.定义一个循环,用于判断单链表是否有环
        while (fast != null && fast.getNext() != null) {
            // 5.设置快慢指针每次往后移动
            fast = fast.getNext().getNext();
            slow = slow.getNext();
            // 6.如果 fast 和 slow 指向的是同一个节点,则证明单链表有环
            if (fast == slow) {
                return fast;
            }
        }
        // 7.执行到此处,证明单链表是无环单链表
        return null;
    }
}

4.12、判断两个单链表相交的第一个交点(https://leetcode-cn.com/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/)

package com.hr;

/**
 * 获得两个单链表相交的第一个交点
 */
public class Test11 {
    public static void main(String[] args) {
        // 1.创建两个单链表
        Node lastNode = new Node(77);
        Node node6 = new Node(66, lastNode);
        Node node3 = new Node(33, node6);
        Node node2 = new Node(22, node3);
        Node head1 = new Node(11, node2);
        Node node5 = new Node(55, node6);
        Node head2 = new Node(44, node5);
        // 2.获得两个单链表相交的第一个交点
        Node commonNode = getFirstCommonNode(head1, head2);
        System.out.println(commonNode.getData());
    }

    /**
     * 获得两个单链表相交的第一个交点
     * 49
     *
     * @param head1 单链表 1 的首节点
     * @param head2 单链表 2 的首节点
     * @return 返回两个单链表相交的第一个交点
     */
    public static Node getFirstCommonNode(Node head1, Node head2) {
        // 1.处理 head1 或 head2 为 null 的情况
        if (head1 == null || head2 == null) {
            return null;
        }
        // 2.获得以 head1 为首节点的单链表长度
        int length1 = getLength(head1);
        // 3.获得以 head2 为首节点的单链表长度
        int length2 = getLength(head2);
        // 4.定义 longNode 指针,用于指向长度较长单链表的首节点
        Node longNode = length1 > length2 ? head1 : head2;
        // 5.定义 shortNode 指针,用于指向长度较短单链表的首节点
        Node shortNode = length1 > length2 ? head2 : head1;
        // 6.让 longNode 指针往后移动 Math.abs(length1-length2)次
        for (int i = 0; i < Math.abs(length1 - length2); i++) {
            longNode = longNode.getNext();
        }
        // 7.定义一个循环,每次让 longNode 和 shortNode 往后移动一次
        while (longNode != shortNode) {
            longNode = longNode.getNext();
            shortNode = shortNode.getNext();
        }
        // 8.返回两个单链表相交的第一个交点
        return longNode;
    }

    /**
     * 获得单链表的长度
     *
     * @param headNode 单链表的首节点
     * @return 返回单链表的长度
     */
    public static int getLength(Node headNode) {
        // 1.定义一个变量,用于保存单链表的长度
        int length = 0;
        // 2.定义一个循环,用于实现单链表的遍历操作
        Node tempNode = headNode;
        while (tempNode != null) {
            tempNode = tempNode.getNext();
        // 3.更新 length 的值
            length++;
        }
        // 4.返回单链表的长度
        return length;
    }
}

4.13、复杂链表的复制(https://leetcode-cn.com/problems/fu-za-lian-biao-de-fu-zhi-lcof/)

package com.hr;

/**
 * 复杂链表的复制
 */
public class Test12 {
    public static void main(String[] args) {
        // 1.创建一个复杂链表
        Node lastNode = new Node("E");
        Node node4 = new Node("D", lastNode);
        Node node3 = new Node("C", node4);
        Node node2 = new Node("B", node3);
        Node headNode = new Node("A", node2);
        headNode.random = node3;
        node2.random = lastNode;
        node4.random = node2;
        // 2.复杂链表的复制
        Node cloneNode = cloneNode(headNode);
        System.out.println();
    }

    /**
     * 复杂链表的复制
     *
     * @param headNode 复杂链表的首节点
     * @return 返回复制后复杂链表的首节点
     */
    public static Node cloneNode(Node headNode) {
        // 1.处理 headNode 为 null 的情况
        if (headNode == null) {
            return null;
        }
        // 2.复制复杂链表中的每一个节点,并且把复制的节点添加到被复制节点的后面
        Node curNode = headNode;
        while (curNode != null) {
            // 创建一个复制节点(N')
            Node node = new Node(curNode.data);
            // 设置 node 节点的 next 值为 curNode 的后一个节点
            node.next = curNode.next;
            // 设置 curNode 的 next 值为 node
            curNode.next = node;
            // 更新 curNode 的值
            curNode = node.next;
        }
        // 3.设置每一个复制节点的 random 指针
        curNode = headNode;
        while (curNode != null) {
            // 获得复制节点(N')
            Node node = curNode.next;
            // 处理 curNode 的 random 指针不为 null 的情况
            if (curNode.random != null) {
                // 设置 node 的 random 值为 curNode.random 的下一个节点
                node.random = curNode.random.next;
            }
            // 更新 curNode 的值
            curNode = node.next;
        }
        // 4.把长链表拆分为两个链表
        // 定义一个节点,用于保存拷贝后复杂链表的首节点
        Node head = headNode.next;
        curNode = headNode;
        while (curNode != null) {
            // 获得复制节点(N')
            Node node = curNode.next;
            // 设置 curNode 的 next 值为 node.next
            curNode.next = node.next;
            // 设置 node 的 next 值为 node.next.next
            node.next = node.next == null ? null : node.next.next;
            // 更新 curNode 的值
            curNode = curNode.next;
        }
        // 5.返回拷贝后复杂链表的首节点
        return head;
    }

    /**
     * 节点类
     */
    private static class Node {
        /**
         * 用于保存节点中的数据
         */
        private Object data;
        /**
         * 用于保存指向下一个节点的地址值
         */
        private Node next;
        /**
         * 用于保存指向任意节点的地址值
         */
        private Node random;

        /**
         * 专门为 data 做初始化工作
         *
         * @param data
         */
        public Node(Object data) {
            53
            this.data = data;
        }

        /**
         * 专门为 data 和 next 做初始化工作
         *
         * @param data
         * @param next
         */
        public Node(Object data, Node next) {
            this.data = data;
            this.next = next;
        }
    }
}

我是小白弟弟,一个在互联网行业的小白,立志成为一名架构师
https://blog.csdn.net/zhouhengzhe?t=1

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhz小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值