• 6L-单向链表实现


    关注公众号 MageByte,有你想要的精彩内容。文中涉及的代码可访问 GitHub:https://github.com/UniqueDong/algorithms.git

    上一篇《链表导论心法》讲解了链表的理论知识以及链表操作的实现原理。talk is cheap, show me the code ! 今天让我以一起把代码撸一遍,在写代码之前一定要根据上一篇的原理多画图才能写得好代码。举例画图,辅助思考。

    废话少说,撸起袖子干。

    接口定义

    首先我们定义链表的基本接口,为了显示出 B 格,我们模仿我们 Java 中的 List 接口定义。

    package com.zero.algorithms.linear.list;
    
    public interface List<E> {
    
        /**
         * Returns <tt>true</tt> if this list contains no elements.
         *
         * @return true if this list contains no elements
         */
        boolean isEmpty();
    
        /**
         * Returns the number of elements in this list.
         *
         * @return the number of elements in this list
         */
        int size();
    
        /**
         * Returns the element at the specified position in this list.
         * @param index index of the element to return
         * @return the element at the specified position in this list
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        E get(int index);
    
        /**
         * Appends the specified element to the end of this list (optional operation).
         *
         * @param e element to be appended to this list
         * @return
         */
        boolean add(E e);
    
        /**
         * Inserts the specified element at the specified position in this list
         * (optional operation).  Shifts the element currently at that position
         * (if any) and any subsequent elements to the right (adds one to their
         * indices).
         *
         * @param index   index at which the specified element is to be inserted
         * @param element element to be inserted
         */
        void add(int index, E element);
    
        /**
         * return true if this list contains the specified element
         *
         * @param o element whose presence in this list is to be tested
         * @return true if this list contains the specified element
         */
        boolean contains(Object o);
    
        /**
         * Removes the first occurrence of the specified element from this list,
         * if it is present (optional operation).
         *
         * @param o element to be removed from this list, if present
         * @return <tt>true</tt> if this list contained the specified element
         */
        boolean remove(Object o);
    
        /**
         * Removes the element at the specified position in this list (optional
         * operation).  Shifts any subsequent elements to the left (subtracts one
         * from their indices).  Returns the element that was removed from the
         * list.
         *
         * @param index the index of the element to be removed
         * @return the element previously at the specified position
         */
        E remove(int index);
    
        /**
         * Returns the index of the first occurrence of the specified element
         * in this list, or -1 if this list does not contain the element.
         * More formally, returns the lowest index <tt>i</tt> such that
         * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
         * or -1 if there is no such index.
         *
         * @param o element to search for
         * @return the index of the first occurrence of the specified element in
         * this list, or -1 if this list does not contain the element
         */
        int indexOf(Object o);
    }
    
    

    抽象链表模板

    看起来是不是很像骚包,接着我们再抽象一个抽象类,后续我们还会继续写双向链表,循环链表。双向循环链表。把他们的共性放在抽象类中,将不同点延迟到子类实现。

    package com.zero.algorithms.linear.list;
    
    public abstract class AbstractList<E> implements List<E> {
        protected AbstractList() {
        }
    
        public abstract int size();
    
        /**
         * Inserts the specified element at the beginning of this list.
         *
         * @param e the element to add
         */
        public abstract void addFirst(E e);
    
        @Override
        public boolean isEmpty() {
            return size() == 0;
        }
    
        public void add(int index, E element) {
            throw new UnsupportedOperationException();
        }
    
        public E remove(int index) {
            throw new UnsupportedOperationException();
        }
    
        /**
         * check index
         * @param index to check
         */
        public final void checkPositionIndex(int index) {
            if (!isPositionIndex(index))
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    
        /**
         * Tells if the argument is the index of a valid position for an
         * iterator or an add operation.
         */
        private boolean isPositionIndex(int index) {
            return index >= 0 && index <= size();
        }
    
        /**
         * Constructs an IndexOutOfBoundsException detail message.
         * Of the many possible refactorings of the error handling code,
         * this "outlining" performs best with both server and client VMs.
         */
        private String outOfBoundsMsg(int index) {
            return "Index: " + index + ", Size: " + size();
        }
    
        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();
        }
    
    }
    
    

    Node 节点

    单向链表的每个节点有两个字段,一个用于保存当前节点的数据 item,另外一个则是 指向下一个节点的 next 指针。所以我们在单向链表定义一个静态内部类表示 Node 节点。

    构造方法分别将数据构建成 Node 节点,默认 next 指针是 null

        /**
         * Node data
         *
         * @param <E>
         */
        private static class Node<E> {
            /**
             * Node of data
             */
            E item;
            /**
             * pointer to next Node
             */
            Node<E> next;
    
            public Node(E item, Node<E> next) {
                this.item = item;
                this.next = next;
            }
    
            public Node(E item) {
                this(item, null);
            }
        }
    
    

    单项链表,size 属性保存当前节点数量,Node<E> head指向 第一个节点的指针,并且存在

    (head == null && last == null) || (head.prev == null && head.item !=null)
    

    是真事件。以及 last指针指向最后的节点。最后我们还要重写 toString 方法,便于测试。

    代码如下所示:

    public class SingleLinkedList<E> extends AbstractList<E> {
    
        transient int size = 0;
        /**
         * pointer to head node
         */
        transient Node<E> head;
        /**
         * pointer to last node
         */
        transient Node<E> last;
    
        public SingleLinkedList() {
        }
        
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            Node<E> cur = this.head;
            while (Objects.nonNull(cur)) {
                sb.append(cur.item).append("->");
                cur = cur.next;
            }
            sb.append("NULL");
            return sb.toString();
        }
    }
    

    添加元素

    添加元素可能存在三种情况:

    1. 头结点添加。
    2. 中间任意节点添加。
    3. 尾节点添加。

    在尾节点添加

    将数据构造成 newNode 节点,将原先的 last 节点 next 指向 newNode 节点,如果 last 节点是 null 则将 head 指向 newNode ,同时 size + 1

    public boolean add(E e) {
            linkLast(e);
            return true;
    }
        /**
         * Links e as last element
         *
         * @param e data
         */
        private void linkLast(E e) {
            // 先取出原 last node
            final Node<E> l = last;
            final Node<E> newNode = new Node<>(e);
            // 修改 last 指针到新添加的 node
            last = newNode;
            // 如果原 last 是 null 则将 newNode 设置为 head,不为空则原 last.next 指针 = newNode
            if (Objects.isNull(l)) {
                head = newNode;
            } else {
                l.next = newNode;
            }
            size++;
        }
    

    在头结点添加

    将数据构造成 newNode 节点,同时 newNode.next 指向原先 head节点,并将 head 指向 newNode 节点,如果 head == null 标识当前链表还没有数据,将 last 指针指向 newNOde 节点。

        @Override
        public void addFirst(E e) {
            final Node<E> h = this.head;
            // 构造新节点,next 指向原先的 head
            final Node<E> newNode = new Node<>(e, h);
            head = newNode;
            // 如果原先的 head 节点为空,则 last 指针也指向新节点
            if (Objects.isNull(h)) {
                last = newNode;
            }
            size++;
        }
    

    在指定位置添加

    分两种情况,当 index = size 的时候意味着在最后节点添加,否则需要找到当前链表指定位置的节点元素,并在该元素前面插入新的节点数据,重新组织两者节点的 next指针。

    linkLast 请看前面,这里主要说明 linkBefore。

    首先我们先查询出 index 位置的 Node 节点,并将 newNode.next 指向 该节点。同时还需要找到index 位置的上一个节点,将 pred.next = newNode。这样就完成了节点的插入,我们只要画图辅助思考就很好理解了。

        @Override
        public void add(int index, E element) {
            checkPositionIndex(index);
            if (index == size) {
                linkLast(element);
            } else {
                linkBefore(element, index);
            }
        }
        /**
         * Inserts element e before non-null Node of index.
         */
        void linkBefore(E element, int index) {
            final Node<E> newNode = new Node<>(element, node(index));
            if (index == 0) {
                head = newNode;
            } else {
                Node<E> pred = node(index - 1);
                pred.next = newNode;
            }
            size++;
        }
    
        /**
         * Returns the (non-null) Node at the specified element index.
         */
        Node<E> node(int index) {
            Node<E> x = this.head;
            for (int i = 0; i < index; i++) {
                x = x.next;
            }
            return x;
        }
    

    判断是否存在

    indexOf(Object o) 用于查找元素所在位置,若不存在则返回 -1。

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

    查找方法

    根据 index 查找该位置的节点数据,其实就是遍历该链表。所以这里的时间复杂度是 O(n)。

        @Override
        public E get(int index) {
            return node(index).item;
        }
    

    删除节点

    删除有两种情况,分别是删除指定位置的节点和根据数据找到对应的节点删除。

    先来看第一种根据 index 删除节点:

    先检验 index 是否合法,然后根据 index 找到待删除 node。这里的删除比较复杂,老铁们记得多画图来辅助理解,防止出现指针指向错误造成意想不到的结果。

        @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();
        }
    

    最难理解的就是后面这段代码了,但是只要多画图,多思考就好多了。

    1. x 节点就是当前要删除的节点数据,我们先把该节点保存的 item 以及 next 指针保存到临时变量。 第 10 ~13 这块 while 循环主要是用于找出被删除节点的 引用 cur 以及上一个节点的引用 prev。
    2. 在找到被删除的 cur 指针以及 cur 上一个节点指针 prev 后我们做删除操作,这里有三种情况:当链表只有一个节点的时候,当一个以上节点情况下分为删除头结点、尾节点、其他节点。
        /**
         * Unlinks non-null node x.
         */
        E unlink(Node<E> x) {
            final E item = x.item;
            final Node<E> next = x.next;
            Node<E> cur = head;
            Node<E> prev = null;
            // 寻找被删除 node cur 节点以及 cur 的上一个节点 prev
            while (!cur.item.equals(item)) {
                prev = cur;
                cur = cur.next;
            }
            // 当只有一个节点的时候 prev  = null,next = null
            // 如果删除的是头结点,则 head = x.next,否则 prev.next = next 打断与被删除节点的联系
            if (prev == null) {
                head = next;
            } else {
                prev.next = next;
            }
            // 如果删除最后一个节点,则 last 指向 prev,否则打断被删除的节点 next = null
            if (next == null) {
                last = prev;
            } else {
                x.next = null;
            }
    
            size--;
            x.item = null;
            return item;
        }
    

    单元测试

    我们使用 junit做单元测试,引入maven 依赖。

    <dependencies>
            <dependency>
                <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter-engine</artifactId>
                <version>5.5.2</version>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter-params</artifactId>
                <version>5.5.2</version>
                <scope>test</scope>
            </dependency>
    
        </dependencies>
    

    创建测试用例

    package com.zero.linkedlist;
    
    import com.zero.algorithms.linear.list.SingleLinkedList;
    import org.junit.jupiter.api.*;
    import org.junit.jupiter.params.ParameterizedTest;
    import org.junit.jupiter.params.provider.ValueSource;
    
    @DisplayName("单向链表测试")
    public class SingleLinkedListTest {
        SingleLinkedList<Integer> singleLinkedList = new SingleLinkedList<>();
    
        @BeforeAll
        public static void init() {
        }
    
        @AfterAll
        public static void cleanup() {
        }
    
        @BeforeEach
        public void tearup() {
            System.out.println("当前测试方法开始");
            System.out.println(singleLinkedList.toString());
        }
    
        @AfterEach
        public void tearDown() {
            System.out.println("当前测试方法结束");
            System.out.println(singleLinkedList.toString());
        }
    
        @DisplayName("add 默认末尾添加")
        @Test
        void testAdd() {
            singleLinkedList.add(1);
            singleLinkedList.add(2);
        }
    
        @DisplayName("在指定位置添加 add")
        @Test
        void testAddIndex() {
            singleLinkedList.add(0, 1);
            singleLinkedList.add(0, 0);
            singleLinkedList.add(1, 2);
        }
    
        @DisplayName("addFirst 表头添加测试")
        @Test
        void testAddFirst() {
            singleLinkedList.addFirst(0);
            singleLinkedList.addFirst(1);
        }
    
        @DisplayName("contains 测试")
        @ParameterizedTest
        @ValueSource(ints = {1})
        void testContains(int e) {
            singleLinkedList.add(e);
            boolean contains = singleLinkedList.contains(e);
            Assertions.assertTrue(contains);
        }
    
        @DisplayName("testIndexOf")
        @ParameterizedTest
        @ValueSource(ints = {3})
        void testIndexOf(int o) {
            singleLinkedList.add(1);
            singleLinkedList.add(2);
            singleLinkedList.add(3);
            int indexOf = singleLinkedList.indexOf(o);
            Assertions.assertEquals(2, indexOf);
    
        }
    
        @DisplayName("test Get")
        @Test
        void testGet() {
            singleLinkedList.addFirst(3);
            singleLinkedList.addFirst(2);
            singleLinkedList.addFirst(1);
            Integer result = singleLinkedList.get(1);
            Assertions.assertEquals(2, result);
        }
    
        @DisplayName("testRemoveObject 删除")
        @Test
        void testRemoveObjectWithHead() {
            singleLinkedList.add(1);
            singleLinkedList.add(2);
            singleLinkedList.add(3);
            singleLinkedList.addFirst(0);
            singleLinkedList.remove(0);
            singleLinkedList.remove(Integer.valueOf(3));
            singleLinkedList.remove(Integer.valueOf(2));
            singleLinkedList.remove(Integer.valueOf(1));
        }
    
    }
    
    

    到这里单向链表的代码就写完了,我们一定要多写才能掌握指针打断的正确操作,尤其是在删除操作最复杂。

    课后思考

    1. Java 中的 LinkedList 是什么链表结构呢?
    2. 如何 java 中的LinkedList 实现一个 LRU 缓存淘汰算法呢?

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

    MageByte

  • 相关阅读:
    JZOJ 1075. 【GDKOI2006】新红黑树
    [CQOI2018]异或序列
    JZOJ 1077. 【GDKOI2006】防御力量
    [TJOI2014]匹配
    JZOJ 1073. 【GDOI2005】山海经
    JZOJ 3745. 【NOI2014模拟7.14】Problem A
    SQL Server 2008 数据库快照
    vSphere 5.0 开发系列(一)vSphere 5.0 环境搭建手顺
    Crm系统帮助
    SQL Server 2008 Entity Data Model 和 Linq to Entities
  • 原文地址:https://www.cnblogs.com/WeaRang/p/12672867.html
Copyright © 2020-2023  润新知