• 7L-双向链表实现


    链表是基本的数据结构,尤其双向链表在应用中最为常见,LinkedList 就实现了双向链表。今天我们一起手写一个双向链表。

    文中涉及的代码可访问 GitHub:https://github.com/UniqueDong/algorithms.git

    上次我们说了「单向链表」的代码实现,今天带大家一起玩下双向链表,双向链表的节点比单项多了一个指针引用 「prev」。双向链表就像渣男,跟「前女友」和「现女友」,还有一个「备胎』都保持联系。前女友就像是前驱节点,现女友就是 「当前 data」,而「next」指针就像是他套住的备胎。每个 Node 节点有三个属性,类比就是 「前女友」+ 「现女友」 + 「备胎」。

    使用这样的数据结构就能实现「进可攻退可守」灵活状态。

    接下来让我们一起实现『渣男双向链表』。

    定义Node

    节点分别保存现女友、前女友、跟备胎的联系方式,这样就能够实现一三五轮换运动(往前看有前女友,往后看有备胎),通过不同指针变可以找到前女友跟备胎。就像渣男拥有她们的联系方式。

    
        private static class Node<E> {
            //现女友
            E item;
            // 备胎
            Node<E> next;
            // 前女友
            Node<E> prev;
    
            public Node(Node<E> prev, E item, Node<E> next) {
                this.prev = prev;
                this.item = item;
                this.next = next;
            }
        }
    

    代码实现

    定义好渣男节点后,就开始实现我们的双向链表。类似过来就是一个渣男联盟排成一列。我们还需要定义两个指针分别指向头结点和尾节点。一个带头大哥,一个收尾小弟。

    public class DoubleLinkedList<E> extends AbstractList<E> implements Queue<E> {
        transient int size = 0;
    
        /**
         * Pointer to first node.
         * Invariant: (first == null && last == null) ||
         * (first.prev == null && first.item != null)
         */
        transient Node<E> first;
    
        /**
         * Pointer to last node.
         * Invariant: (first == null && last == null) ||
         * (last.next == null && last.item != null)
         */
        transient Node<E> last;
    }
    

    头节点添加

    新的渣男进群了,把他设置成群主带头大哥。首先构建新节点,prev = null,带头大哥业务繁忙,不找前女友,所以 prev = null;next 则指向原先的 first。

    1. 如果链表是空的,则还要把尾节点也指向新创建的节点。
    2. 若果链表已近有数据,则把原先 first.prev = newNode。
        @Override
        public void addFirst(E e) {
            linkFirst(e);
        }
        /**
         * 头结点添加数据
         *
         * @param e 数据
         */
        private void linkFirst(E e) {
            final Node<E> f = this.first;
            Node<E> newNode = new Node<>(null, e, f);
            // first 指向新节点
            first = newNode;
            if (Objects.isNull(f)) {
                // 链表是空的
                last = newNode;
            } else {
                // 将原 first.prev = newNode
                f.prev = newNode;
            }
            size++;
        }
    

    尾节点添加

    将新进来的成员放在尾巴。

    第一步构建新节点,把 last 指向新节点。

    第二步判断 last 节点是否是空,为空则说明当前链表是空,还要把 first 指向新节点。否则就需要把原 last.next 的指针指向新节点。

        @Override
        public boolean add(E e) {
            addLast(e);
            return true;
        }
        private void addLast(E e) {
            final Node<E> l = this.last;
            Node<E> newNode = new Node<>(l, e, null);
            last = newNode;
            if (Objects.isNull(l)) {
                // 链表为空的情况下,设置 first 指向新节点
                first = newNode;
            } else {
                // 原 last 节点的 next 指向新节点
                l.next = newNode;
            }
            size++;
        }
    
    

    指定位置添加

    分为两种情况,一个是在最后的节点新加一个。一种是在指定节点的前面插入新节点。

    在后面添加前面尾巴添加已经说过,对于在指定节点的前面插入需要我们先找到指定位置节点,然后改变他们的 prev next 指向。

    
        @Override
        public void add(int index, E element) {
            checkPositionIndex(index);
            if (index == size) {
                linkLast(element);
            } else {
                linkBefore(element, node(index));
            }
        }
    
    
        /**
         * Links e as last element.
         */
        void linkLast(E element) {
            addLast(element);
        }
    
    
        /**
         * Inserts element e before non-null Node succ.
         */
        private void linkBefore(E element, Node<E> succ) {
            // assert succ != null
            final Node<E> prev = succ.prev;
            // 构造新节点
            final Node<E> newNode = new Node<>(prev, element, succ);
            succ.prev = newNode;
            if (Objects.isNull(prev)) {
                first = newNode;
            } else {
                prev.next = newNode;
            }
            size++;
        }
    

    节点查找

    为了优化,根据 index 查找的时候先判断 index 落在前半部分还是后半部分。前半部分通过 first 开始查找,否则通过 last 指针从后往前遍历。

        @Override
        public E get(int index) {
            checkElementIndex(index);
            return node(index).item;
        }
    
        /**
         * Returns the (non-null) Node at the specified element index.
         */
        Node<E> node(int index) {
            // 优化查找,判断 index 在前半部分还是后半部分。
            if (index < (this.size >> 2)) {
                // 前半部分,从头结点开始查找
                Node<E> x = this.first;
                for (int i = 0; i < index; i++) {
                    x = x.next;
                }
                return x;
            } else {
                // 后半部分,从尾节点开始查找
                Node<E> x = this.last;
                for (int i = size - 1; i > index; i--) {
                    x = x.prev;
                }
                return x;
            }
        }
    

    查找 Object 所在位置 indexOf ,若找不到返回 -1

        @Override
        public int indexOf(Object o) {
            int index = 0;
            if (Objects.isNull(o)) {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item == null) {
                        return index;
                    }
                    index++;
                }
            } else {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item.equals(o)) {
                        return index;
                    }
                    index++;
                }
            }
            return -1;
        }
    

    判断 链表中是否存在 指定对象 contains ,其实还是利用 上面的 indexOf 方法,当返回值 不等于 -1 则说明包含该对象。

    
        @Override
        public boolean contains(Object o) {
            return indexOf(o) != -1;
        }
    

    节点删除

    有两种删除情况:

    1. 根据下标删除指定位置的节点。
    2. 删除指定数据的节点。

    删除指定位置节点

    1. 首先判断该 index 是否合法存在。
    2. 查找要删除的节点位置,重新设置被删除节点关联的指针指向。

    node() 方法已经在前面的查找中封装好这里可以直接调用,我们再实现 unlink 方法,该方法还会用于删除指定对象,所以这抽出来实现复用。也是最核心最不好理解的方法,我们多思考画图理解下。

        @Override
        public E remove(int index) {
            checkElementIndex(index);
            return unlink(node(index));
        }
        public final void checkElementIndex(int index) {
            if (!isElementIndex(index))
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
        /**
         * Tells if the argument is the index of an existing element.
         */
        private boolean isElementIndex(int index) {
            return index >= 0 && index < size();
        }
    
        /**
         * Unlinks non-null node x.
         */
        private E unlink(Node<E> x) {
            // assert x != null;
            final E element = x.item;
            final Node<E> next = x.next;
            final Node<E> prev = x.prev;
            // 若 只有一个节点,那么会执行 prev == null 和 next == null 分支代码
            // 若 prev == null 则说明删除的是头结点,主要负责 x 节点跟前驱节点的引用处理
            if (Objects.isNull(prev)) {
                first = next;
            } else {
                prev.next = next;
                x.prev = null;
            }
            // 若 next 为空,说明删除的是尾节点,主要负责 x 与 next 节点 引用的处理
            if (Objects.isNull(next)) {
                last = prev;
            } else {
                next.prev = prev;
                x.next = null;
            }
    
            x.item = null;
            size--;
            return element;
        }
    

    分别找出被删除节点 x 的前驱和后继节点,要考虑当前链表只有一个节点的情况,最后还要把被删除节点的 的 next 指针 ,item 设置 null,便于垃圾回收,防止内存泄漏。

    删除指定数据

    这里判断下数据是否是 null , 从头节点开始遍历链表,当找到索要删除的节点的时候低啊用前面封装好的 unlink 方法实现删除。

    
        @Override
        public boolean remove(Object o) {
            if (Objects.isNull(o)) {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item == null) {
                        unlink(x);
                        return true;
                    }
                }
            } else {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (o.equals(x.item)) {
                        unlink(x);
                        return true;
                    }
                }
            }
            return false;
        }
    

    完整代码可以参考 GitHub:https://github.com/UniqueDong/algorithms.git

    加群跟我们一起探讨,欢迎关注 MageByte,我第一时间解答。

    MageByte

    推荐阅读

    1.跨越数据结构与算法

    2.时间复杂度与空间复杂度

    3.最好、最坏、平均、均摊时间复杂度

    4.线性表之数组

    5.链表导论-心法篇

    6.单向链表正确实现方式

    原创不易,觉得有用希望读者随手「在看」「收藏」「转发」三连。

  • 相关阅读:
    MLPclassifier,MLP 多层感知器的的缩写(Multi-layer Perceptron)
    linux 内存不足时候 应该及时回收page cache
    关闭swap的危害——一旦内存耗尽,由于没有SWAP的缓冲,系统会立即开始OOM
    使用Networkx进行图的相关计算——黑产集团挖掘,我靠,可以做dns ddos慢速攻击检测啊
    ARIMA模型实例讲解——网络流量预测可以使用啊
    http://www.secrepo.com 安全相关的数据获取源
    什么是HTTP Referer?
    列举某域名下所有二级域名的方法
    HMM(隐马尔科夫模型)——本质上就是要预测出股市的隐藏状态(牛市、熊市、震荡、反弹等)和他们之间的转移概率
    成都优步uber司机第五组奖励政策
  • 原文地址:https://www.cnblogs.com/WeaRang/p/12697355.html
Copyright © 2020-2023  润新知