数据结构与算法之双向链表的设计与实现

前言

文章链接之前所介绍的是单向链表,查找元素只能从头节点开始寻找,判断出符合条件的元素,时间复杂度为O(n)。当链表节点数目过多时,查询性能下降。而有了双向链表后,我们可以从两个方向查询元素,提升查询效率。

一、双向链表

1.1 概念

双向链表是一种数据结构,由若干个节点构成,其中每个节点均由三部分构成,分别是前驱节点,元素,后继节点。双向链表中的节点在内存中是游离状态存在的,如下图所示:
在这里插入图片描述

1.2 双向链表的应用

1.1 Java集合框架中LinkedList底层就是通过双向链表实现的,我们可以通过查看,阅读源码进行分析。
1.2 MySQL的Innodb存储引擎管理的数据页可以组成一个双向链表,而每个数据页中的记录会按照主键值从小到大的顺序组成一个 单向链表。如下图所示:
在这里插入图片描述

1.3 双向链表的node方法

查询方式:对半查找,若查找的位置小于链表长度的一半,则从头结点开始顺序查找;否则,从尾结点开始逆序查找,这样做可以提高查询效率。

//根据索引找到节点(查找目标节点位置小于链表长度的一半就从前往后找,大于的话从后往前找话)
private Node<E> node(int index) {
    rangeCheck(index);
    Node<E> node;
    if (index < (size >> 1)){
        node = first;
        for (int i = 0; i < index; i++) {
            node = node.next;
        }
    }else {
        node = last;
        for (int i = size - 1; i > index; i--) {
            node = node.prev;
        }
    }
    return node;
}

1.4 双向链表的add方法

之前单项链表新增节点操纵需要获取到 index 前面的节点,现在双向链表不需要,可以通过 prev 获取到前驱节点,next 获取到后继节点。

add(E) -- 在链表尾部添加元素,将元素封装到节点中,创建新节点,让新节点和前一个节点建立双向链表的关系。
add(int index,E e) -- 在指定位置插入元素,其过程实际上就是断开链,重新构建链的过程。

在这里插入图片描述
双向链表只有一个节点的时候,如下图所示:
在这里插入图片描述

@Override
public void add(int index, E element) {
    rangeCheckForAdd(index);
    //一开始index == 0 size() == 0 则 old.next == null
    if (size == index){//往最后面添加元素
        //获取到最后一个节点
        Node<E> old = last;
        //创建新的尾节点,last指向新的尾节点
        last = new Node<>(last, element, null);
        if (old == null){
            first = last;
        }else {
            //之前的尾节点的next指向新的尾节点
            old.next = last;
        }
    }else {
        //获取指定index的节点
        Node<E> next = node(index);
        //获取指定index的节点前一个节点
        Node<E> prev = next.prev;
        //创建新节点并指定他的prev和next
        Node<E> node = new Node<>(prev, element, next);
        next.prev = node;
        if (prev == null){
            first = node;
        }else{
            prev.next = node;
        }
    }
    size++;
}

1.5 双向链表的remove方法

remove(int index) – 删除指定位置的元素,其过程实际上依然是断开链,重新构建链的过程。

@Override
public E remove(int index) {
    rangeCheck(index);
    //获取index位置的节点
    Node<E> node = node(index);
    //获取index位置的上一个节点
    Node<E> prev = node.prev;
    //获取index位置的下一个节点
    Node<E> next = node.next;
    //prev == null 则说明删除的是第一个节点 0位置节点
    if (prev == null){
        first = next;
    }else {
        prev.next = next;
    }
     //next == null 则说明删除的是最后一个节点 size()位置节点
    if (next == null){
        last = prev;
    }else{
        next.prev = prev;
    }
    size--;
    return node.element;
}

1.6 整体代码

package com.hbx.linkedList.bidirection;

import com.hbx.module.AbstractList;

public class LinkedList<E> extends AbstractList<E> {

    private Node<E> first;
    private Node<E> last;

    // 链表中的节点
    private static class Node<E> {
        E element; // 节点元素
        Node<E> prev; // 节点指向下一个节点
        Node<E> next; // 节点指向下一个节点

        public Node(Node<E> prev, E element, Node<E> next) {
            this.prev = prev;
            this.element = element;
            this.next = next;
        }

        @Override
        public String toString(){
            StringBuilder sb = new StringBuilder();
            if(prev != null){
                sb.append(prev.element);
            }else{
                sb.append("null");
            }
            sb.append("_").append(element).append("_");
            if(next != null){
                sb.append(next.element);
            }else{
                sb.append("null");
            }

            return sb.toString();
        }
    }

    @Override
    public void clear() {
        size = 0;
        //jvm的gcRoots(可达性分析)
        first = null;
        last = null;
    }

    @Override
    public E get(int index) {
        return node(index).element;
    }
    /**
     * 根据索引找到节点
     */
    private Node<E> node(int index) {
        rangeCheck(index);
        if (index < (size >> 1)) { // 索引小于一半从前往后找
            Node<E> node = first;
            for (int i = 0; i < index; i++) {
                node = node.next;
            }
            return node;
        } else { // 索引大于一半从后往前找
            Node<E> node = last;
            for (int i = size - 1; i > index; i--) {
                node = node.prev;
            }
            return node;
        }
    }

    @Override
    public E set(int index, E element) {
        /*
         * 最好:O(1)
         * 最坏:O(n)
         * 平均:O(n)
         */
        E old = node(index).element;
        node(index).element = element;
        return old;
    }

    @Override
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        //一开始index == 0 size() == 0 old.next == null
        if (size == index){//往最后面添加元素
            Node<E> old = last;
            last = new Node<>(old, element, null);
            if (old == null){
                first = last;
            }else {
                old.next = last;
            }
        }else {
            Node<E> next = node(index);
            Node<E> prev = next.prev;
            Node<E> node = new Node<>(prev, element, next);
            next.prev = node;
            if (prev == null){
                first = node;
            }else{
                prev.next = node;
            }
        }
        size++;
    }

    @Override
    public E remove(int index) {
        rangeCheck(index);
        Node<E> node = node(index);
        Node<E> prev = node.prev;
        Node<E> next = node.next;
        if (prev == null){
            first = next;
        }else {
            prev.next = next;
        }
        if (next == null){
            last = prev;
        }else{
            next.prev = prev;
        }
        size--;
        return node.element;
    }

    @Override
    public int indexOf(E element) {
        // 有个注意点, 如果传入元素为null, 则不能调用equals方法, 否则会空指针
        // 因此需要对元素是否为null做分别处理
        Node<E> node = first;
        if (element == null) {
            for (int i = 0; i < size; i++) {
                if (node.element == null)
                    return i;
                node = node.next;
            }
        } else {
            for (int i = 0; i < size; i++) {
                if (node.element.equals(element))
                    return i;
                node = node.next;
            }
        }
        return ELEMENT_NOT_FOUND;
    }

    @Override
    public String toString() {
        StringBuilder string = new StringBuilder();
        string.append("[size=").append(size).append(", ");
        Node<E> node = first;
        for (int i = 0; i < size; i++) {
            if (i != 0) {
                string.append(", ");
            }
            string.append(node);
            node = node.next;
        }
        string.append("]");
        return string.toString();
    }

}

1.7 接口测试

@Test
public void test(){
    List<Integer> list = new LinkedList<>();
    list.add(11);
    list.add(22);
    list.add(33);
    list.add(44);
    list.add(0,55);//55, 11,22, 33, 44
    list.add(2,66);//55, 11, 66, 22, 33, 44
    list.add(list.size(),77);//55, 11, 66, 22, 33, 44, 77

    list.remove(0);
    list.remove(2);
    list.remove(list.size()-1);
    System.out.println(list);
}

在这里插入图片描述

二、对比学习

2.1 单向链表 vs 双向链表

粗略对比一下删除的操作数量:操作数量缩减了近一半
在这里插入图片描述

2.2 双向链表 vs 动态数组

动态数组:开辟、销毁内存空间的次数相对较少,但可能造成内存空间浪费(可以通过缩容解决)
双向链表:开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费。

  • 如果频繁在尾部进行添加、删除操作,动态数组、双向链表均可选择。
  • 如果频繁在头部进行添加、删除操作,建议选择使用双向链表。
  • 如果有频繁的(在任意位置)添加、删除操作,建议选择使用双向链表。
  • 如果有频繁的查询操作(随机访问操作),建议选择使用动态数组。

2.3 ArrayList和LinkedList的区别

  1. ArrayList底层是数组实现的,LinkedList底层是双向链表,二者的数据结构是不同的。
  2. 因为数据结构不同,所以最终的性能是不同的;
    查询元素:
    ArrayList是根据下标查找元素,查询效率非常高,时间复杂度为O(1)。
    LinkedList中若查找头部/尾部的元素,其查询效率还是比较高的,但是若查找偏中间位置的元素,其查询效率是比较低下的。
    增删元素:
    ArrayList:若在尾部添加增删元素,此时性能可能会很高,在头部和中间位置进行增删操作,其效率都不是很高。
    LinkedList:若在头尾部分增删元素,此时性能很高,但是若在偏中间位置进行增删元素,此时性能不高的(因为增删,会先查询指定位置的节点,查询效率是低下的)。
    整体而言:
    ArrayList查询性能是高于LinkedList的,但是若LinkedList进行头尾查询,此时效率也是非常高的。
    若进行的是偏头部和尾部的增删操作,选择LinkedList,而若对其他位置进行增删,此时ArrayList和LinkedList的效率是差不多的。
    ArrayList查询方面性能更好,增删方面,除了头尾增删,其他增删和LinkedList差不多,所以经常使用ArrayList。
    面试标准回答:
    ArrayList查询性能更高,LinkedList进行头尾增删,性能很高,除此之外,其他增删,ArrayList和LinkedList差不多。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值