【数据结构】双链表

链表(二)

00 引入

衔接上文单链表,相较于本篇将要讲的双链表,单链表有以下弱势:

  1. 难以反向遍历:由于单链表只包含一个指针,即指向下一个节点的指针,无法直接访问前一个节点。因此,在单链表中反向遍历需要从头节点开始顺序遍历到目标节点,效率相对较低。
  2. 难以在任意位置快速插入和删除:在双链表中,可以通过两个指针的操作快速定位到目标节点的前后节点,从而在O(1)时间复杂度内进行插入和删除操作。而在单链表中,为了插入或删除目标节点,需要先找到目标节点的前一个节点,并修改其指针指向,操作相对复杂,时间复杂度为O(n)。
  3. 难以在尾部追加节点:由于单链表只有一个指针指向下一个节点,如果要在单链表的尾部追加节点,就需要遍历整个链表找到尾节点,然后进行操作。而双链表在尾部追加节点只需要修改尾节点的指针,操作更加简单和高效。

那么接下来就让我们来实现一下非循环双向链表。

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

01 类的搭建

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QcuqlesW-1692202718939)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230816161634477.png)]

示例代码如下:

static class ListNode{
        private int val;
        private ListNode prev;
        private ListNode next;

        public ListNode(int val) {
            this.val = val;
        }
    }
    public ListNode head;//定义双向链表的头节点
    public ListNode last;//定义双向链表的尾巴

02 得到链表的长度

这个其实就是遍历一下链表,和单链表的操作没有区别。

示例代码如下:

//得到单链表的长度
    public int size(){
        ListNode cur = this.head;
        int count = 0;
        while(cur != null){
            count++;
            cur = cur.next;
        }
        return count;
    }

03 打印链表

原则如02操作。

示例代码如下:

public void display(){
        ListNode cur = this.head;
        while(cur != null){
            System.out.println(cur + "->");
            cur = cur.next;
        }
        System.out.println("null");
    }

04 查找是否包含关键字key是否在链表当中

原则如02 03操作

示例代码如下:

//查找是否包含关键字key是否在链表当中
    public boolean contains(int key){
        ListNode cur =this.head;
        while(cur != null){
            if (cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

05 头插法

注意考虑点:

  1. 考虑空链表的情况:如果链表为空,即没有任何节点,那么插入的节点将成为新的头节点。在这种情况下,需要特殊处理头节点的前后指针。
  2. 更新头节点的前驱指针:在头插法中,插入的节点将成为新的头节点,所以需要更新原头节点的前驱指针,让它指向新的头节点。
  3. 更新新头节点的后继指针:插入的节点作为新的头节点,它的后继指针需要指向原来的头节点,以连接链表的其他节点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-svd1z80U-1692202718941)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230816173601219.png)]

示例代码如下:

//头插法
    public void addFirst(int data){
        ListNode node = new ListNode(data);
        if(head == null){
            head = node;
            last = node;
        }else {
            node.next = head;
            head.prev = node;
            head = node;
        }
    }

06 尾插法

注意点:

  1. 考虑空链表的情况:如果链表为空,即没有任何节点,那么插入的节点将成为新的头节点。在这种情况下,需要特殊处理头节点的前后指针。
  2. 更新尾节点的后继指针:在尾插法中,插入的节点将成为新的尾节点,所以需要更新原尾节点的后继指针,让它指向新的尾节点。
  3. 更新新尾节点的前驱指针:插入的节点作为新的尾节点,它的前驱指针需要指向原来的尾节点,以连接链表的其他节点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lMOLYOpQ-1692202718941)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230816175501258.png)]

示例代码:

//尾插法
    public void addLast(int data){
        ListNode node = new ListNode(data);
        if (head == null){
            head = node;
            last = node;
        }else {
            last.next = node;
            node.prev = last;
            last = node;
        }
    }

07 任意位置插入

注意点:

  1. 判断插入位置是否合法:首先要确保插入的位置在链表的长度范围内,即在 0 到链表长度的范围之间。
  2. 更新插入节点的前驱指针和后继指针:在进行任意位置插入时,需要更新插入节点的前驱指针和后继指针,使其正确指向前一个节点和后一个节点。
  3. 更新前后节点的指针:需要更新前一个节点和后一个节点的后继指针和前驱指针,让它们正确地连接到插入节点上。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xGTg5bi9-1692202718942)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230816222255142.png)]

示例代码:

//任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        checkIndex(index);
        if(index == 0){
            addFirst(data);
            return;
        }
        else if (index == size()){
            addLast(data);
            return;
        }
        
        ListNode node = new ListNode(data);
        ListNode cur = head;
        while(index != 0){
            cur = cur.next;
            index--;
        }
        node.next = cur;
        cur.prev.next = node;
        node.prev = cur.prev;
        cur.prev = node;

    }

    private void checkIndex(int index){
        if (index < 0 || index > size()){
            throw new IndexOutOfException("index 不合法");
        }
    }

08 删除关键字为key的节点

  1. 查找要删除的节点:首先需要在双链表中找到第一次出现关键字为key的节点。遍历链表,逐个比较节点的值,直到找到目标节点或遍历到链表末尾。
  2. 更新前后节点的指针:找到目标节点后,需要更新前一个节点和后一个节点的后继指针和前驱指针,让它们正确地连接起来。
  3. 处理删除头节点的情况:如果需要删除头节点,需要特殊处理。即使要删除的节点是头节点,也要正确更新头指针。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R2SwLt4Z-1692202718942)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230816234333333.png)]

示例代码:

//删除第一次出现关键字为key的节点
    public void remove(int key){
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) {
                //删除头节点
                if (cur == head) {
                    head = head.next;
                    if (head != null) {
                        //考虑只有一个节点的情况
                        head.prev = null;
                    }else {
                        last = null;
                    }
                } else {
                    //删除中间节点以及尾巴节点
                    if (cur.next != null) {
                        //中间节点
                        cur.prev.next = cur.next;
                        cur.next.prev = cur.prev;
                    } else {
                        //尾巴节点
                        cur.prev.next = cur.next;
                        last = last.prev;
                    }
                }
                return;
            } else {
                cur = cur.next;
            }
        }
    }

09 删除所有值为key的节点

这个与08其实大差不差。

示例代码:

//删除所有值为key的节点
    public void removeAllKey(int key){
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) {
                //删除头节点
                if (cur == head) {
                    head = head.next;
                    if (head != null) {
                        //考虑只有一个节点的情况
                        head.prev = null;
                    }else {
                        last = null;
                    }
                } else {
                    //删除中间节点以及尾巴节点
                    if (cur.next != null) {
                        //中间节点
                        cur.prev.next = cur.next;
                        cur.next.prev = cur.prev;
                    } else {
                        //尾巴节点
                        cur.prev.next = cur.next;
                        last = last.prev;
                    }
                }
                //return;
                //区别所在
                cur = cur.next;
            } else {
                cur = cur.next;
            }
        }
    }

唯一的区别就是,在寻找出第一个关键字key之后继续往后走cur = cur.next,继续删,直到删完为止。

10 清空

使用一个循环来遍历双链表中的每个节点,并且可以选择释放每个节点所占用的内存。最后,将头节点指针设置为null,以清空链表。

public void clear(){
        ListNode cur = head;
        while(cur != null){
            ListNode curNext = cur.next;
            cur.prev = null;
            cur.next = null;
            cur = curNext;
        }
        head = null;
        last = null;
    }

11 LinkedList常规一些操作

import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

/**
 * @date 2023/8/16
 */
public class Test {

    public static void main(String[] args) {
        List<Integer> list = new LinkedList<>();
        list.add(1);
        list.add(1);
        list.add(1);
        list.add(1);
        System.out.println(list);
        for(int x : list) {
            System.out.println(x);
        }

        System.out.println("=====");
        ListIterator<Integer> it =  list.listIterator();
        while (it.hasNext()) {
            System.out.print(it.next()+" ");
        }
        System.out.println();
        System.out.println("=====");
        ListIterator<Integer> it2 =  list.listIterator(list.size());
        while (it2.hasPrevious()) {
            System.out.print(it2.previous()+" ");
        }
        System.out.println();
    }
}

在这里插入图片描述

12 ArrayList与LinkedList的区别

  1. 内部实现:ArrayList是基于数组实现的动态数组,而LinkedList是基于双向链表实现的。因此,在插入或删除元素时,ArrayList需要移动数组中的元素,而LinkedList只需要改变节点的指针。
  2. 访问效率:由于ArrayList是基于数组实现的,它可以通过索引直接访问元素,因此在随机访问元素时效率较高。而LinkedList需要从头节点或尾节点开始遍历链表,因此随机访问的效率较低。
  3. 插入和删除效率:在插入或删除元素时,ArrayList需要移动元素来保持数组的连续性,因而在特定位置的插入和删除操作的效率较低。而`LinkedList``只需要改变节点的指针,因此在特定位置的插入和删除操作的效率较高。
  4. 空间占用:由于ArrayList是基于数组实现的,它需要一段连续的内存空间来存储元素,因此在使用期间其大小是固定的。而LinkedList每个节点都需要额外的空间来存储前后节点的指针,因此在空间占用方面相对较大。

综上所述,ArrayList适用于有频繁的随机访问操作和插入/删除较少的场景,而LinkedList适用于有频繁的插入/删除操作和随机访问较少的场景。根据具体的应用场景和需求,可以选择合适的集合类。


那么至此,关于链表的一些总结到此暂时完结撒花🎊🎊🎊🎊🎊🎊,接下来会学习栈和队列,MySQL,以及不定时的算法总结,其实有额外时间的话,准备详细聊聊C中的动态内存管理以及结构体之类的知识。

  • 17
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值