数据结构:手撕链表

1. 简介

链表也是最基础的数据结构,属于线性表。链表就像火车一样,每一个车厢互相连接,这些车厢就是一个个结点(Node)。链表就是通过这些结点的连接形成的。

对比于数组,链表不支持随机访问,所以数组的访问速度非常快,而链表就慢了。但是链表的长度是动态的,这一点比数组好,不会浪费空间。

2. 创建链表

把结点Node封装在类中,因为用户是不需要知道有Node结点这些概念。

public class LinkedList<E> {
    // 结点
    private class Node{
        public E data;
        public Node next;

        public Node(E data, Node next) {
            this.data = data;
            this.next = next;
        }

        public Node(E data) {
            this(data, null);
        }

        public Node() {
            this(null, null);
        }
        
        @Override
        public String toString() {
            return data.toString();
        }
    }
}

3. 链表的添加

在添加之前,我们需要去访问,因为链表中没有索引这种概念,那访问就需要一个头结点head,从头结点开始访问。

public class LinkedList<E> {
    // 结点
    private class Node{
        ...
    }
    // 新增
    private Node head;
    // 链表长度
    private int size;
    
    public LinkedList() {
        head = null;
        size = 0;
    }
    
    /**
     * 获取链表中的元素个数
     * @return
     */
    public int getSize() {
        return size;
    }

    /**
     * 判断链表是否为空
     * @return
     */
    public boolean isEmpty() {
        return size == 0;
    }
}


如图:主要有三步:

  • 第一步先创建一个node结点并把值传进去;
  • 第二步把新创建的结点的next指针指向head;
  • 第三步把head指向node结点。

在这里插入图片描述

    /**
     * 添加操作
     * @param data
     */
    public void addFirst(E data){
        // Node node = new Node(data);
        // node.next = head;
        // head = node;
        // 前面三条语句可以结合成一条
        head = new Node(data, head);
        size++;
    }

有些书上的头结点不存储值。其实头结点可以存储值也可不存储,无论如何就是一个标记,根据该标记方便我们可以操作链表。当然头结点不存值的情况代码需要修改,下面会说。

4. 链表的插入操作

现在假设链表从头结点到尾,可用索引0,1,2…表示,那么假如要把一个结点node插入到索引2,则需要怎么操作。

注意:链表中没有索引的,这里只是为了演示插入操作,因为该操作是一个非常重要的思维。

首先能想到的是,先去查询该位置,查询是利用头结点head,但必须创建一个头结点head的副本来查询,因为头结点head只能一直标记头结点。我们需要查询出该索引的前一个位置的结点,记为prev。

然后将node的next指向prev:

在这里插入图片描述

最后将prev的next指向node,就成功插入了:
在这里插入图片描述
这两条顺序的顺序不可换,可以试试换了两条语句顺序后的结果,就是错误的:
在这里插入图片描述

需要注意,当如果要从索引0插入时,要怎么办,头结点可没有前一个结点。这可以调用addFirst方法。

    /**
     * 插入操作
     * index 范围为0到size
     * 链表中是没有索引的概念,该操作只能理解思维
     * @param index
     * @param data
     */
    public void insert(int index, E data){
        if(index < 0 || index > size){
            throw new IllegalArgumentException("Insert failed. Illegal index.");
        }

        if(index == 0){
            addFirst(data);
        } else {
            Node prev = head;
            for(int i = 0; i < index - 1; i++){
                prev = prev.next;
            }

            // Node node = new Node(data);
            // node.next = prev.next;
            // prev.next = node;
            // 另一种写法,就是上面三句的结合
            prev.next = new Node(data, prev.next);

            size++;
        }
    }

上面的代码还可以修改,比如如果超出size,那么可以把插入的结点添加到链表尾。

现在写个末尾添加结点的方法:

    /**
     * 添加到尾部
     * @param data
     */
    public void addLast(E data){
        insert(size, data);
    }

这些东西都是涉及到引用的知识,比如查询:

    // 创建一个head副本
    Node prev = head;
    for(int i = 0; i < index - 1; i++){
        // 此时改变引用指向,并不会影响到head
        prev = prev.next;
    }

如果这样写:

    // 不创建head副本
    for(int i = 0; i < index - 1; i++){
        // 此时改变引用指向,那就影响到了head的指向,即前面的结点会丢失,永远找不回来。
        head = head.next;
    }

5. 链表改为使用虚拟头结点

有些书用的头结点不放任何东西时,每次添加结点是添加到头节点后面的,该头结点称为虚拟头结点,记为dummyHead,比如:
在这里插入图片描述

思路都是一样的,其实这是一个插入操作。因为每次都知道要插入的位置的前一个结点,所以完全不需要索引的概念,现在可以来写下,另一种添加操作:(把刚刚的head改成dummyHead

    // 把 head名称改为 dummyHead
    private Node dummyHead;
    // 修改构造函数
    public LinkedList() {
        // 创建虚拟头结点
        dummyHead = new Node(null);
        size = 0;
    }
    
      /**
     * 插入操作
     * index 范围为0到size
     * 链表中是没有索引的概念,该操作只能理解思维
     * 插入操作的关键点在于:找到目标位置的前一个位置
     * @param index
     * @param data
     */
    public void insert(int index, E data){
        if(index < 0 || index > size){
            throw new IllegalArgumentException("Insert failed. Illegal index.");
        }
        // 此时head是虚拟头结点
        Node prev = dummyHead;
        // 注意边界
        for(int i = 0; i < index; i++){
            prev = prev.next;
        }

        // Node node = new Node(data);
        // node.next = prev.next;
        // prev.next = node;
        // 另一种写法
        prev.next = new Node(data, prev.next);

        size++;

    }
    
    /**
     * 添加头结点操作
     * @param data
     */
    public void addFirst(E data){
        insert(0, data);
    }

跟前面的添加操作对比看看:

  • 前面的添加操作,每次添加的结点都当成头结点head。
  • 这里的添加操作,每次添加的结点都放在头结点head后面。

两种方法都可以使用任意一种。现在我们把前面的代码换成使用虚拟头结点来做

5. 链表的查询

为了练习还是引入索引。

注意查询是要查询哪个结点,像前面的插入操作是为了查询某位置的前一个结点。下面的查询是为了查询某位置的结点。

查询有两种方式,一种使用while,一种使用for。

    /**
     * 获取链表第index个元素(0~size)
     * @param index
     * @return
     */
    public E get(int index) {
        if(index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Illegal index.");
        }
        // 查找第index位置的结点
        Node cur = dummyHead.next;
        for(int i = 0; i < index; i++) {
            cur = cur.next;
        }

        return cur.data;
    }

    /**
     * 获取首结点的值
     * @return
     */
    public E getFirst() {
        return get(0);
    }

    /**
     * 获取尾结点的值
     * @return
     */
    public E getLast() {
        return get(size - 1);
    }
    
    /**
     * 查询链表中是否包含元素data
     * @param data
     * @return
     */
    public boolean contains(E data) {
        Node cur = dummyHead.next;
        while(cur != null) {
            if(cur.data.equals(data)) {
                return true;
            }
            cur = cur.next;
        }
        
        return false;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        
        // 第一种,使用while
        // Node cur = dummyHead.next;
        // while(cur != null) {
        //     sb.append(cur + "->");
        //     cur = cur.next;
        // }

        // 第二种,使用for
        for(Node cur = dummyHead.next; cur != null; cur = cur.next) {
            sb.append(cur + "->");
        }

        sb.append("NULL");
        
        return sb.toString();
    }

最好测试一下:

    public static void main(String[] args) {
        LinkedList<Integer> linkedList = new LinkedList<>();
        
          for(int i = 0; i < 5; i++) {
            linkedList.addFirst(i+1);
            System.out.println(linkedList);
        }
        System.out.println("新添加:");
        linkedList.insert(2, 5);
        linkedList.addLast(6);

        System.out.println(linkedList);
        System.out.println("首结点元素:" + linkedList.getFirst());
        System.out.println("尾结点元素:" + linkedList.getLast());
        System.out.println("是否包含元素1:" + linkedList.contains(1));

    }

6. 链表的修改

无非还是先查询,在修改。

    /**
     * 为了练习还是引入索引。
     * 表示修改index位置的结点的元素
     * @param index 索引
     * @param data 要修改的值
     */
    public  void set(int index, E data) {
        if(index < 0 || index >= size) {
            throw new IllegalArgumentException("Set failed. Index is illegal");
        }
        // 查询
        Node cur = dummyHead.next;
        for(int i = 0; i < index; i++) {
            cur = cur.next;
        }
        
        cur.data = data;
    }

测试:

    public static void main(String[] args) {
        LinkedList<Integer> linkedList = new LinkedList<>();

        for(int i = 0; i < 5; i++) {
            linkedList.addFirst(i+1);
            System.out.println(linkedList);
        }


        System.out.print("修改索引为2的元素,改为10:");
        linkedList.set(2, 10);
        System.out.println(linkedList);
    }

8. 链表的删除

还是引入索引方便演示,假设要删除索引为2的结点。

每个结点都有一个next,在查询时是根据这个next来找到下一个结点。所以通过索引1结点的next就可以找到索引2的结点,以此为前提,那么只要让索引1结点的next不指向索引2的结点,不就找不到索引2的结点了吗,这不就是删除了吗。所以我们得先找到要删除的结点的前一个结点,记为prev

但是我们还得保证索引2的结点后面的结点不丢失,所以可以把索引1结点的next指向索引2结点的next。这就删除了。

看看图:
第一步:初始状态
在这里插入图片描述

第二步:查询,找到delNode的前一个结点
在这里插入图片描述

第三步:删除
在这里插入图片描述

小优化:可以看到delNode虽然是删了,但是还没有被垃圾挥手器回收,因为delNode还是有引用。所以我们主动把delNode指向null。
在这里插入图片描述

代码:

    /**
     * 删除
     * @param index
     * @return 返回删除元素
     */
    public E remove(int index) {
        if(index < 0 || index >= size) {
            throw new IllegalArgumentException("delete failed. Index is illegal");
        }
        // 待删除的结点之前的结点
        Node prev = dummyHead;
        for(int i = 0; i < index; i++) {
            prev = prev.next;
        }
        // 要删除的结点
        Node delNode = prev.next;
        // 删除
        prev.next = delNode.next;
        delNode.next = null;

        size--;

        return delNode.data;
    }

    /**
     * 删除首结点
     * @return 返回删除元素
     */
    public E removeFirst() {
        return remove(0);
    }

    /**
     * 删除尾结点
     * @return 返回删除元素
     */
    public E removeLast() {
        return remove(size - 1);
    }
    
    /**
     * 从链表中删除元素e
     * @param data
     */
    public void removeElement(E data){

        Node prev = dummyHead;
        while(prev.next != null){
            if(prev.next.data.equals(data))
                break;
            prev = prev.next;
        }

        if(prev.next != null){
            Node delNode = prev.next;
            prev.next = delNode.next;
            delNode.next = null;
        }
    }

测试:

   public static void main(String[] args) {

        LinkedList<Integer> linkedList = new LinkedList<>();

        for(int i = 0; i < 5; i++) {
            linkedList.addFirst(i+1);
            System.out.println(linkedList);
        }
        System.out.println("新添加:");
        linkedList.insert(2, 5);
        linkedList.addLast(6);

        System.out.println(linkedList);
        System.out.println("首结点元素:" + linkedList.getFirst());
        System.out.println("尾结点元素:" + linkedList.getLast());
        System.out.println("是否包含元素1:" + linkedList.contains(1));

        System.out.print("修改索引为2的元素,改为10:");
        linkedList.set(2, 10);
        System.out.println(linkedList);

        // 删除
        System.out.println("删除索引为2的元素" + linkedList.remove(2));
        System.out.println("删除首结点" + linkedList.removeFirst());
        System.out.println("删除尾结点" + linkedList.removeLast());

        System.out.println(linkedList);
    }

9. 复杂度分析

  • 添加操作:总体为O(n)

    • addFirst():O(1)
    • addLast():O(n)
    • insert(index):平均情况下index可能在前半部分也可能在后半部分,所以均摊起来为:O(n/2)=O(n)
  • 删除操作:总体为O(n)

    • removeFirst():O(1)
    • removeLast():O(n)
    • remove(index):平均情况下index可能在前半部分也可能在后半部分,所以均摊起来为:O(n/2)=O(n)
  • 修改操作:总体为O(n)

    • set(idnex, data):O(n)
  • 查找操作:总体为O(n)

    • getFirst(index):O(1)
    • getLast():O(n)
    • get(index):O(n)
    • contains():O(n)

相对于数组来说,链表的总体时间复杂度确实是比数组差,因为在知道索引的情况下,数组支持随机访问。

但是,我们可以发现,如果链表只在头结点操作,那么对于增,删,查的操作都是O(1),而且可以知道链表其实不去修改好

所以现在如果只对头结点操作,那么链表的总体复杂度跟数组差不多,而且比数组好的就是,链表是动态的,不会浪费空间

10. 链表的全部代码

public class LinkedList<E> {
    // 结点
    private class Node{
        public E data;
        public Node next;

        public Node(E data, Node next) {
            this.data = data;
            this.next = next;
        }

        public Node(E data) {
            this(data, null);
        }

        public Node() {
            this(null, null);
        }

        @Override
        public String toString() {
            return data.toString();
        }
    }

    private Node dummyHead;
    private int size;

    public LinkedList() {
        // 创建虚拟头结点
        dummyHead = new Node(null);
        size = 0;
    }

    /**
     * 获取链表中的元素个数
     * @return
     */
    public int getSize() {
        return size;
    }

    /**
     * 判断链表是否为空
     * @return
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 插入操作
     * index 范围为0到size
     * 链表中是没有索引的概念,该操作只能理解思维
     * 插入操作的关键点在于:找到目标位置的前一个位置
     * @param index
     * @param data
     */
    public void insert(int index, E data) {
        if(index < 0 || index > size){
            throw new IllegalArgumentException("Insert failed. Illegal index.");
        }
        // 此时head是虚拟头结点,查找index位置的前一个结点
        Node prev = dummyHead;
        // 注意边界
        for(int i = 0; i < index; i++){
            prev = prev.next;
        }

        // Node node = new Node(data);
        // node.next = prev.next;
        // prev.next = node;
        // 另一种写法
        prev.next = new Node(data, prev.next);

        size++;

    }

    /**
     * 添加到虚拟头结点的下一个位置
     * @param data
     */
    public void addFirst(E data) {
        // Node node = new Node(data);
        // node.next = head;
        // head = node;
        // 前面三条语句可以结合成一条
        // head = new Node(data, head);
        //
        // size++;
        insert(0, data);
    }

    /**
     * 添加到尾部
     * @param data
     */
    public void addLast(E data){
        insert(size, data);
    }



    /**
     *
     * @param index 索引
     * @return 获取链表第index个元素(0~size)
     */
    public E get(int index) {
        if(index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Illegal index.");
        }
        // 查找第index位置的结点
        Node cur = dummyHead.next;
        for(int i = 0; i < index; i++) {
            cur = cur.next;
        }

        return cur.data;
    }

    /**
     * @return 获取首结点的值
     */
    public E getFirst() {
        return get(0);
    }

    /**
     * @return 获取尾结点的值
     */
    public E getLast() {
        return get(size - 1);
    }

    /**
     * 查询链表中是否包含元素data
     * @param data
     * @return
     */
    public boolean contains(E data) {
        Node cur = dummyHead.next;
        while(cur != null) {
            if(cur.data.equals(data)) {
                return true;
            }
            cur = cur.next;
        }

        return false;
    }

    /**
     * 为了练习还是引入索引。
     * 表示修改index位置的结点的元素
     * @param index 索引
     * @param data 要修改的值
     */
    public  void set(int index, E data) {
        if(index < 0 || index >= size) {
            throw new IllegalArgumentException("Set failed. Index is illegal");
        }
        // 查询
        Node cur = dummyHead.next;
        for(int i = 0; i < index; i++) {
            cur = cur.next;
        }

        cur.data = data;
    }

    /**
     * 删除
     * @param index
     * @return 返回删除元素
     */
    public E remove(int index) {
        if(index < 0 || index >= size) {
            throw new IllegalArgumentException("delete failed. Index is illegal");
        }
        // 待删除的结点之前的结点
        Node prev = dummyHead;
        for(int i = 0; i < index; i++) {
            prev = prev.next;
        }
        // 要删除的结点
        Node delNode = prev.next;
        // 删除
        prev.next = delNode.next;
        delNode.next = null;

        size--;

        return delNode.data;
    }

    /**
     * 删除首结点
     * @return 返回删除元素
     */
    public E removeFirst() {
        return remove(0);
    }

    /**
     * 删除尾结点
     * @return 返回删除元素
     */
    public E removeLast() {
        return remove(size - 1);
    }
    
    /**
     * 从链表中删除元素e
     * @param data
     */
    public void removeElement(E data){

        Node prev = dummyHead;
        while(prev.next != null){
            if(prev.next.data.equals(data))
                break;
            prev = prev.next;
        }

        if(prev.next != null){
            Node delNode = prev.next;
            prev.next = delNode.next;
            delNode.next = null;
        }
    }
    
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();

        // 第一种,使用while
        // Node cur = dummyHead.next;
        // while(cur != null) {
        //     sb.append(cur + "->");
        //     cur = cur.next;
        // }

        // 第二种,使用for
        for(Node cur = dummyHead.next; cur != null; cur = cur.next) {
            sb.append(cur + "->");
        }

        sb.append("NULL");

        return sb.toString();
    }

    public static void main(String[] args) {

        LinkedList<Integer> linkedList = new LinkedList<>();

        for(int i = 0; i < 5; i++) {
            linkedList.addFirst(i+1);
            System.out.println(linkedList);
        }
        System.out.println("新添加:");
        linkedList.insert(2, 5);
        linkedList.addLast(6);

        System.out.println(linkedList);
        System.out.println("首结点元素:" + linkedList.getFirst());
        System.out.println("尾结点元素:" + linkedList.getLast());
        System.out.println("是否包含元素1:" + linkedList.contains(1));

        System.out.print("修改索引为2的元素,改为10:");
        linkedList.set(2, 10);
        System.out.println(linkedList);

        // 删除
        System.out.println("删除索引为2的元素" + linkedList.remove(2));
        System.out.println("删除首结点" + linkedList.removeFirst());
        System.out.println("删除尾结点" + linkedList.removeLast());

        System.out.println(linkedList);
    }
}

11. 使用链表来实现栈

Stack接口:

public interface Stack<E> {

    int getSize();
    boolean isEmpty();
    void push(E e);
    E pop();
    E peek();
    
}

使用链表实现:

public class LinkedListStack<E> implements Stack<E> {

    private LinkedList<E> linkList;

    public LinkedListStack() {
        linkList = new LinkedList<>();
    }

    @Override
    public int getSize() {
        return linkList.getSize();
    }

    @Override
    public boolean isEmpty() {
        return linkList.isEmpty();
    }

    @Override
    public void push(E e) {
        // 从链表头添加
        linkList.addFirst(e);
    }

    @Override
    public E pop() {
        // 从链表头删除
        return linkList.removeFirst();
    }

    @Override
    public E peek() {
        // 从链表头查看
        return linkList.getFirst();
    }

    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("Stack top :").append(linkList.toString());
        return stringBuilder.toString();
    }

    public static void main(String[] args) {
        LinkedListStack<Integer> stack = new LinkedListStack<>();
        for (int i = 0; i < 5; i++) {
            stack.push(i);
            System.out.println(stack);
        }

        stack.pop();
        System.out.println(stack);

    }
}

虽然基于链表实现的栈在头结点入栈和出栈的时间复杂度都是O(1)。但是链表是需要创建节点的,如果这两个操作的次数很大很大,比如入栈和出栈各1000000次,那么是需要很久的。

12. 使用链表实现队列

我们知道链表如果操作尾结点,那么时间复杂度为O(n)。如果在尾结点加个标记,那么每次操作就不用去找尾节点。该标记跟head一样,我们记为tail。

但是如果要删除尾结点,必须遍历一次链表,因为要找到删除尾结点的前一个结点,即使有tail也无法改变。

所以我们可以在链表首做队列头,而在链表尾做队列尾。这样,我们在入队和出队的两个操作的时间复杂度都是O(1)。

比如添加操作:

  • 先让head和tail初始化为null。
  • 只有一个结点是特殊情况:此时tail和head共同指向同一个结点。
  • 当添加第二个结点时,规定是在tail后面添加的,并且此时就要改变tail的指向。
    在这里插入图片描述

代码:

public interface Queue<E> {

    int getSize();
    boolean isEmpty();
    void enqueue(E data);
    E dequeue();
    E getFront();
}
public class LinkedListQueue<E> implements Queue<E> {
    // 结点
    private class Node{
        public E data;
        public Node next;

        public Node(E data, Node next) {
            this.data = data;
            this.next = next;
        }

        public Node(E data) {
            this(data, null);
        }

        public Node() {
            this(null, null);
        }

        @Override
        public String toString() {
            return data.toString();
        }
    }

    private Node head, tail;
    private int size;

    public LinkedListQueue() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public void enqueue(E data) {
        if(tail == null) {
            tail = new Node(data);
            head = tail;
        } else {
            tail.next = new Node(data);
            tail = tail.next;
        }
        size++;
    }

    @Override
    public E dequeue() {
        if(isEmpty()) {
            throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
        }

        Node ret = head;
        head = head.next;
        // 如果队列只有一个元素,删除后就没了,要修改tail
        if(head == null) {
            tail = null;
        }
        size--;
        return ret.data;
    }

    @Override
    public E getFront() {
        if(isEmpty()) {
            throw new  IllegalArgumentException("Queue is Empty");
        }

        return head.data;
    }


    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();

        Node cur = head;
        sb.append("Queue top: ");
        while(cur != null) {
            sb.append(cur + "->");
            cur = cur.next;
        }
        sb.append("NULL tail");

        return sb.toString();
    }

    public static void main(String[] args) {
        LinkedListQueue<Integer> queue = new LinkedListQueue<>();
        for(int i = 0; i < 5; i++){
            queue.enqueue(i);
            System.out.println(queue);

            if(i % 3 == 2){
                queue.dequeue();
                System.out.println(queue);
            }
        }
    }
}

使用链表实现的队列比使用数组实现的队列性能更好。因为数组一定有一头要O(n)。当然数组可以修改成循环数组来解决此问题。

发布了69 篇原创文章 · 获赞 65 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_41800884/article/details/104569881