顺序表和链表04——双向链表

目录

前言

引入

接口实现:

总代码参考


前言

单链表∶单向链表,默认只能从链表的头部遍历到链表的尾部实际的应用中很少见,太局限,只能从头遍历到尾部
 

引入

所以为了解决这个问题,就引入了双向链表这个概念

双向链表:对于该链表中的任意节点,即可通过该节点向后走,也可以通过该节点向前走。

双向链表实际工程中应用非常广泛,使用链表这个结构的首选

JDK -> LinkedList ->双向链表
JDK里面 LinkedList 用的就是双向链表

双向链表的优点:

每个节点既保存了下一个节点的地址,也保存了上—个节点的地址
这样的话,就可以通过任意的节点从前向后或者从后向前

接口实现:

/**
 * 增加
 *  addFirst(int val)  头插
 *  add(int index, int val) 在任意索引index处插入节点
 *  addLast(int val) 尾插
 *
 * 查找
 *  getByValue(int val) 查询第一个值为 val的索引为多少 不存在则返回 -1
 *  get(int index) 查询索引为index处的节点值为多少
 *  contains(int val) 查询是否包含指定值的节点
 *
 *  修改
 *  set(int index, int newVal) 修改索引为index位置的结点值为 newVal,返回修改前的节点值
 *
 *  删除
 *   remove(int index) 删除 index 处的节点,并返回删除的节点值
 *   removeFrist() 头删
 *   removeLast()  尾删
 *   removeValueOnce(int val)  删除第一个值为val的节点
 *   removeAllValue(int val)  删除全部值为 val 的节点
 */

开始实行 

/**
 * 单链表的具体的每个节点 - 车厢类
 */

准备工作:

/**
 * 单链表的具体的每个节点 - 车厢类
 */
class DoubleNode {

    // 前驱节点
    DoubleNode prve;
    // 当前节点值
    int val;
    // 后继节点
    DoubleNode next;

    // alt + insert 快捷键

    //无参构造
    public DoubleNode() {
    }

    //有参构造
    public DoubleNode(int val) {
        this.val = val;
    }

    //有参构造
    public DoubleNode(DoubleNode prve, int val, DoubleNode next) {
        this.prve = prve;
        this.val = val;
        this.next = next;
    }
}
/**
 * 基于int的双向链表 - 火车
 * 真正被用户使用的是火车类-双向链表对象
 */

准备工作

public class Double_Linked_list {

    // 有效节点的个数
    private int size;
    // 当前头节点
    private DoubleNode head;
    // 当前尾节点
    private DoubleNode tail;

}

1. 实现头插法:分为链表中有无节点两种情况

    // 在双向链表的头部插入新节点
    public void addFirst(int val){
        //直接用构造方法快速赋值
        DoubleNode node = new DoubleNode(null,val,head);
        
        if(head == null || tail == null){
            //链表中没有节点时
            head = tail = node;
        }else{
            //以前的头节点的前驱地址连接新的节点位置
            head.prve = node;
            head = node;//新节点成为新的头节点
        }

        size++;
    }

 2.补充toString方法打印链表

    //补充toString方法打印链表
    public String toString(){
        String rep = "";
        for (DoubleNode x = head; x != null ; x = x.next) {
            rep += x.val;
            rep += " -> ";
        }
        rep += "NULL";
        return rep;
    }

测试:

//双向链表的测试模块

public class Double_Test {
    public static void main(String[] args) {

        Double_Linked_list dll = new Double_Linked_list();
        dll.addFirst(1);
        dll.addFirst(2);
        dll.addFirst(3);
        dll.addFirst(4);
        System.out.println(dll);
    }
}

3. 精简下刚刚写的头插法。 

你会发现,无论链表中是否存在节点,因为是头插都会有 head = node 这一步所以可以放在下面。而且也因为是头插,就执行判断此时的尾节点是否为空就行了。

    // 在双向链表的头部插入新节点
    public void addFirst(int val){
        //直接用构造方法快速赋值
        DoubleNode node = new DoubleNode(null,val,head);

//        if(head == null || tail == null){
//            //链表中没有节点时
//            head = tail = node;
//        }else{
//            //以前的头节点的前驱地址连接新的节点位置
//            head.prve = node;
//            head = node;//新节点成为新的头节点
//        }

        //精简写法
        if(tail == null){
           // 链表中没有节点时
            tail = node;
        }else{
            head.prve = node;//以前的头节点的前驱地址连接新的节点位置
        }
        head = node; // 对于头插来说,最终无论链表是否为空。head = node
        size++;
    }


4. 在双向链表的尾部插入新节点

现在为什么先写尾插而不是 index任意插?

因为之前的插入一个节点都是找前驱,头节点没有前驱所以先写了头插法。

而这次的双向,因为有了尾节点的存在所以可以找节点的后继开始插入节点,但尾节点是没有后继的,所以先写的尾插法,跟头插差不多。(当然按以前的写法也是可以的,但既然有了尾节点当然要利用起来,为了方便后面写 index任意插)

    // 在双向链表的尾部插入新节点
    public void addLast(int val){
        DoubleNode node = new DoubleNode(tail, val, null);//直接用构造方法快速赋值
        if(head == null){
            head = node;
        }else{
            tail.next = node;
        }
        tail = node;
        size ++;
    }

 

测试:

public class Double_Test {
    public static void main(String[] args) {

        Double_Linked_list dll = new Double_Linked_list();
        dll.addLast(1);
        dll.addLast(2);
        dll.addLast(3);
        dll.addLast(4);
        System.out.println(dll);
    }
}

 

5. 在index索引处插入节点

 

之前我们都是找到前驱,此时找前驱是不可以灵活一点?

之前只能从head开始向后变量
假设咱链表有100个节点,我想在97号索引位置插入新元素,如果还是从头找那么我们弄这个双向链表的意义在哪

从尾节点向前遍历就会快的多
 

我到底从前向后找还是从后向前找呢,那就设定一个条件
index < size / 2    =>从前向后,插入位置在前半部分
index > size / 2    =>从后向前,插入位置就在后半部分链表

发现:找前驱结点不光在插入会用到删除也会用到,所以为了方法,直接拉出来写成一个方法。

功能:根据索引值返回对于节点的地址。

注释:要用private封装,因为这个方法只是给我们实现功能提供的,并不是给别人使用的。私有。

    //根据索引值返回对于节点的地址,不存在则返回 null
    private DoubleNode node(int index){
        DoubleNode x = null;
        if(index < size / 2){
          //从前往后走找需要 index 步
          x = head;
            for (int i = 0; i < index; i++) {
                x = x.next;
            }
        }else{
            //从后往前走找需要 size - 1 - index 步
            x = tail;
            for (int i = size - 1; i > index ; i--) {
                //注意:这里不再是用next了而且前地址prve
                 x = x.prve;
            }
        }
        return x;
    }

现在可以开始写 在index索引处插入节点 add方法

    /**
     * 在 index 索引处插入一个节点
     */
    public void add(int index, int val){
        //判断索引的合法性
        if(index < 0 || index > size){
            System.out.println("插入的索引值错误");
            return;
        }
        if(index == 0){
            addFirst(val);
        }else if(index == size){
            addLast(val);
        }else{
            //到这里说明此时至少有三个节点
            DoubleNode node = node(index);//索引位置处的节点。
            DoubleNode cur = new DoubleNode(node.prve, val, node);//新节点
            //在new这个对象的时候就已经用构造方法连接好两条线了。
            node.prve.next = cur;//前驱连接新节点
            node.prve = cur;//待插入位置的节点连接新节点
            size ++;
        }
    }

测试:

public class Double_Test {
    public static void main(String[] args) {

        Double_Linked_list dll = new Double_Linked_list();
        dll.addLast(1);
        dll.addLast(2);
        dll.addLast(3);
        dll.addLast(4);
        System.out.println(dll);
        dll.add(2,6);
        System.out.println(dll);
    }
}

6. 查询

6.1 查询第一个值为 val的索引为多少

6.2  查询索引为index处的节点值为多少

6.3 查询是否包含指定值的节点

注释:为了写程序方便,把判断索引合理性写成一个方法  rangeCheck

    /**
     * 查询第一个值为 val的索引为多少
     * 不存在则返回 -1
     */
    public int getByValue(int val){
        DoubleNode rep = head;
        for (int i = 0; i < size; i++) {
                if(rep.val == val){
                    return i;
                }
             rep = rep.next;
        }

        return -1;  //循环里面没找到就代表不存在
    }

    /**
     * 查询索引为index处的节点值为多少
     */
    public int get(int index){
        if(rangeCheck(index)){
         return node(index).val;
        }
        return -1;
    }

    /**
     * 查询是否包含指定值的节点
     */
    public boolean contains(int val){
        return getByValue(val) != -1;
    }

    /**
     * 判断给的index索引是否合理
     */
    private boolean rangeCheck(int index){
        if(index < 0 || index >= size){
            return false;
        }
        return true;
    }

 测试:

//双向链表的测试模块

public class Double_Test {
    public static void main(String[] args) {

        Double_Linked_list dll = new Double_Linked_list();
        dll.addLast(1);
        dll.addLast(2);
        dll.addLast(3);
        dll.addLast(4);
        System.out.println(dll);

        System.out.println("查询第一个值为 val的索引为多少:");
        System.out.println(dll.getByValue(4));
        System.out.println(dll.getByValue(1));
        System.out.println(dll.getByValue(5));
        System.out.println("----------------");
        System.out.println("查询索引为index处的节点值为多少:");
        System.out.println(dll.get(0));
        System.out.println(dll.get(3));
        System.out.println(dll.get(4));
        System.out.println("----------------");
        System.out.println("查询是否包含指定值的节点:");
        System.out.println(dll.contains(1));
        System.out.println(dll.contains(4));
        System.out.println(dll.contains(0));
        System.out.println(dll.contains(5));

    }
}

7. 修改索引为index位置的结点值为 newVal,返回修改前的节点值

    public int set(int index, int newVal){
        DoubleNode cur = node(index);
        int rep = cur.val;
        cur.val = newVal;
        return rep;
    }

 测试:

//双向链表的测试模块

public class Double_Test {
    public static void main(String[] args) {

        Double_Linked_list dll = new Double_Linked_list();
        dll.addLast(1);
        dll.addLast(2);
        dll.addLast(3);
        dll.addLast(4);
        System.out.println(dll);
        System.out.println(dll.set(0,6));
        System.out.println(dll);
        System.out.println(dll.set(4,9));
        System.out.println(dll);

    }
}

8. 删除当前双向链表中的node节点

注释:这个方法是私有private封装的,是方便我们等会写删除功能。

如何删除两边中的一个节点?

 这里提出一个很重要的思修: 分治思想

概念:

1. 先处理前驱节点的事儿,完全不管后继
2. 等前驱部分全部处理完毕再单独处理后继情况

删除一个节点一共也就四种情况:

 1.前空后空
 2.前不空后空
 3.前不空后不空
 4.前空后不空

注释:删除头节点是 前空后不空,  删除尾节点是 后空前不空

    //删除当前双向链表中的node节点
    private void unlink(DoubleNode node){
        // 1.前空后空
        // 2.前不空后空
        // 3.前不空后不空
        // 4.前空后不空
        DoubleNode prve = node.prve;//前驱
        DoubleNode next = node.next;//后继
        //现在删除就有三种情况,头节点,尾节点.注释:这里的前部分和后部分都是以node为中心的
        //先处理前半部分
        if(prve == null){
            //删除的是头节点,那就更新头节点位置
            head = next;//头节点变为后继节点
            //注释:这里不要着急断开原来头节点的next连接,那算后半部分
            //现在这里只处理前半部分
        }else{
            // 前驱不为空的情况
            prve.next = next;
            node.prve = null;//断开连接
        }

        //现在处理后半部分
        if(next == null){
            //删除的是尾节点,那就更新尾节点位置
            tail = prve;
        }else{
            // 后继不为空的情况
            next.prve = prve;
            node.next = null;//断开连接
        }

        size --;

    }

 注释:

这里说的前半部分和后半部分都是以 node待删除节点为参照物的,node的前部分连线问题和

node后半部分连线问题。

9. 删除 index 处的节点,并返回删除的节点值

和 头删除,尾删。直接利用刚刚写的 unlike 方法

    //删除 index 处的节点,并返回删除的节点值
    public int remove(int index){
        if(rangeCheck(index)){
            DoubleNode node = node(index);//返回索引处的节点地址
            int rep = node.val;
            unlink(node);
            return rep;
        }
        System.out.println("输入删除的索引错误");
        return -1;
    }

    //头删
    public int removeFrist(){
        return remove(0);
    }

    //尾删
    public int removeLast(){ return remove(size - 1); }

10.  最后两个删除val模块

删除出现第一个值为val的结点

删除全部值为 val 的节点

    //删除出现第一个值为val的结点
    public void removeValueOnce(int val){
        for (DoubleNode x = head; x != null ; x = x.next) {
            if(x.val == val){
                unlink(x);
                break;
            }
        }
    }

    //删除全部值为 val 的节点
    public void removeAllValue(int val) {
        for (DoubleNode x = head; x != null; ) {
            if (x.val == val) {
                //如果当前的节点要被删除,保存它下一个节点地址
                DoubleNode successor = x.next;
                unlink(x);
                x = successor;//让循环的x连接上后面的链表
            } else {
                x = x.next;
            }

        }
    }
    //注释:我们unlink删除节点时吧它连接的全断开了
    //要删除全部的val时会发生什么? 当时的在循环中的链表
    //直接失去的后继节点的地址,所以我们要在unlink的时候保存下一个节点地址

注释:这里唯一要注意的就是在我们删除全部val值时,我们的unlink会把当前的node节点连接的线全部断开,导致正在循环的 x 直接就结束了,要解决这个问题就要在unlink删除这个节点前先保存下一个节点的地址。 然后删除后连接后面的链表

测试:

public class Double_Test {
    public static void main(String[] args) {

        Double_Linked_list dll = new Double_Linked_list();
        dll.addLast(3);
        dll.addLast(1);
        dll.addLast(3);
        dll.addLast(1);
        dll.addLast(2);
        dll.addLast(2);
        dll.addLast(2);
        dll.addLast(2);
        dll.addLast(2);
        System.out.println(dll);

        dll.removeValueOnce(1);
        System.out.println(dll);

        dll.removeAllValue(2);
        System.out.println(dll);

    }
}

 

总代码参考

package seqlist.双向链表;
/**
 * 基于int的双向链表 - 火车
 * 真正被用户使用的是火车类-双向链表对象
 */
public class Double_Linked_list {

    // 有效节点的个数
    private int size;
    // 当前头节点
    private DoubleNode head;
    // 当前尾节点
    private DoubleNode tail;

    // 在双向链表的头部插入新节点
    public void addFirst(int val){
        //直接用构造方法快速赋值
        DoubleNode node = new DoubleNode(null,val,head);

//        if(head == null || tail == null){
//            //链表中没有节点时
//            head = tail = node;
//        }else{
//            //以前的头节点的前驱地址连接新的节点位置
//            head.prve = node;
//            head = node;//新节点成为新的头节点
//        }

        //精简写法
        if(tail == null){
           // 链表中没有节点时
            tail = node;
        }else{
            head.prve = node;//以前的头节点的前驱地址连接新的节点位置
        }
        head = node; // 对于头插来说,最终无论链表是否为空。head = node
        size++;
    }

    // 在双向链表的尾部插入新节点
    public void addLast(int val){
        DoubleNode node = new DoubleNode(tail, val, null);//直接用构造方法快速赋值
        if(head == null){
            head = node;
        }else{
            tail.next = node;
        }
        tail = node;
        size ++;
    }

    /**
     * 在 index 索引处插入一个节点
     */
    public void add(int index, int val){
        //判断索引的合法性
        if(index < 0 || index > size){
            System.out.println("插入的索引值错误");
            return;
        }
        if(index == 0){
            addFirst(val);
        }else if(index == size){
            addLast(val);
        }else{
            //到这里说明此时至少有三个节点
            DoubleNode node = node(index);//索引位置处的节点。
            DoubleNode cur = new DoubleNode(node.prve, val, node);//新节点
            //在new这个对象的时候就已经用构造方法连接好两条线了。
            node.prve.next = cur;//前驱连接新节点
            node.prve = cur;//待插入位置的节点连接新节点
            size ++;
        }
    }

    //根据索引值返回对于节点的地址,不存在则返回 null
    private DoubleNode node(int index){
        DoubleNode x = null;
        if(index < size / 2){
          //从前往后走找需要 index 步
          x = head;
            for (int i = 0; i < index; i++) {
                x = x.next;
            }
        }else{
            //从后往前走找需要 size - 1 - index 步
            x = tail;
            for (int i = size - 1; i > index ; i--) {
                //注意:这里不再是用next了而且前地址prve
                 x = x.prve;
            }
        }
        return x;
    }

    /**
     * 查询第一个值为 val的索引为多少
     * 不存在则返回 -1
     */
    public int getByValue(int val){
        DoubleNode rep = head;
        for (int i = 0; i < size; i++) {
                if(rep.val == val){
                    return i;
                }
             rep = rep.next;
        }

        return -1;  //循环里面没找到就代表不存在
    }

    /**
     * 查询索引为index处的节点值为多少
     */
    public int get(int index){
        if(rangeCheck(index)){
         return node(index).val;
        }
        return -1;
    }

    /**
     * 查询是否包含指定值的节点
     */
    public boolean contains(int val){
        return getByValue(val) != -1;
    }

    /**
     * 判断给的index索引是否合理
     */
    private boolean rangeCheck(int index){
        if(index < 0 || index >= size){
            return false;
        }
        return true;
    }

    /**
     * 修改索引为index位置的结点值为 newVal,返回修改前的节点值
     */
    public int set(int index, int newVal){
        DoubleNode cur = node(index);
        int rep = cur.val;
        cur.val = newVal;
        return rep;
    }

    //删除当前双向链表中的node节点
    private void unlink(DoubleNode node){
        // 1.前空后空
        // 2.前不空后空
        // 3.前不空后不空
        // 4.前空后不空
        DoubleNode prve = node.prve;//前驱
        DoubleNode next = node.next;//后继
        //现在删除就有三种情况,头节点,尾节点.注释:这里的前部分和后部分都是以node为中心的
        //先处理前半部分
        if(prve == null){
            //删除的是头节点,那就更新头节点位置
            head = next;//头节点变为后继节点
            //注释:这里不要着急断开原来头节点的next连接,那算后半部分
            //现在这里只处理前半部分
        }else{
            // 前驱不为空的情况
            prve.next = next;
            node.prve = null;//断开连接
        }

        //现在处理后半部分
        if(next == null){
            //删除的是尾节点,那就更新尾节点位置
            tail = prve;
        }else{
            // 后继不为空的情况
            next.prve = prve;
            node.next = null;//断开连接
        }
        size --;
    }

    //删除 index 处的节点,并返回删除的节点值
    public int remove(int index){
        if(rangeCheck(index)){
            DoubleNode node = node(index);//返回索引处的节点地址
            int rep = node.val;
            unlink(node);
            return rep;
        }
        System.out.println("输入删除的索引错误");
        return -1;
    }

    //头删
    public int removeFrist(){
        return remove(0);
    }

    //尾删
    public int removeLast(){ return remove(size - 1); }

    //删除出现第一个值为val的结点
    public void removeValueOnce(int val){
        for (DoubleNode x = head; x != null ; x = x.next) {
            if(x.val == val){
                unlink(x);
                break;
            }
        }
    }

    //删除全部值为 val 的节点
    public void removeAllValue(int val) {
        for (DoubleNode x = head; x != null; ) {
            if (x.val == val) {
                // x就是待删除的结点
                //当前的节点要被删除,保存它下一个节点地址
                DoubleNode successor = x.next;
                unlink(x);
                x = successor;//让循环的x连接上后面的链表
            } else {
                x = x.next;
            }

        }
    }
    //注释:我们unlink删除节点时吧它连接的全断开了
    //要删除全部的val时会发生什么? 当时的在循环中的链表
    //直接失去的后继节点的地址,所以我们要在unlink的时候保存下一个节点地址

    //补充toString方法打印链表
    public String toString(){
        String rep = "";
        for (DoubleNode x = head; x != null ; x = x.next) {
            rep += x.val;
            rep += " -> ";
        }
        rep += "NULL";
        return rep;
    }

}




/**
 * 单链表的具体的每个节点 - 车厢类
 */
class DoubleNode {

    // 前驱节点
    DoubleNode prve;
    // 当前节点值
    int val;
    // 后继节点
    DoubleNode next;

    // alt + insert 快捷键

    //无参构造
    public DoubleNode() {
    }

    //有参构造
    public DoubleNode(int val) {
        this.val = val;
    }

    //有参构造
    public DoubleNode(DoubleNode prve, int val, DoubleNode next) {
        this.prve = prve;
        this.val = val;
        this.next = next;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值