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

大家好我是佩奇
发布于 2022-8-8 19:17
浏览
0收藏

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

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

比如插入节点,在已知节点 b 的前面插入 x

6.单向链表实现的正确方式-鸿蒙开发者社区

废话少说,撸起袖子干。

 

接口定义
首先我们定义链表的基本接口,为了显示出 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));
    }

}

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

 

课后思考
Java 中的 LinkedList 是什么链表结构呢?
如何使用 java 中的LinkedList 实现一个 LRU 缓存淘汰算法呢?

 

 

 

文章转载自公众号:码哥字节

已于2022-8-8 19:17:00修改
收藏
回复
举报
回复
    相关推荐