【数据结构与算法】之链表

一、什么是链表

前言:对链表进行插入和删除操作时,指针一直在变化。在本子上画一画,可以比较容易的理解指针的变化过程。

(一)链表的定义

链表也是多个相同类型 的数据按一定顺序排列的集合,是一种数据储存结构。链表由许多个结点组成,每个结点由存放的数据和指向下一个结点的引用(指针)构成。在单链表中,头指针指向第一个结点,尾指针指向最后一个结点。链表逻辑上是相邻的,但在计算机上的储存却不一定是相邻的。链表作为链式储存结构来实现线性表。
单链表

图一、单链表的结构示意图

(二)链表的分类

单链表:n 个结点链结成一个链表,此链表的每个结点中只包含一个指针,所以叫做单链表。
单链表

图二、单链表

循环链表:将单链表中最后一个结点的指针由指向 null 改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
循环链表

图三、循环链表

双向链表:双向链表是在单链表的每个结点中,再设置一个指向其前一个结点的指针。所以在双向链表中的结点都有两个指针, 一个指向直接后继(后一个结点),另一个指向直接前驱(前一个结点)。
双向链表

图四、双向链表

二、为什么要用链表

(一)链表的优点

  • 不需要提前分配存储空间,存放的元素个数不受限制
  • 增加与删除操作的时间复杂度为 O(1),比数组的 O(N)快。

(二)链表的缺点

  • 对于单链表,需要声明头指针 (head) 和尾指针 (tail),因为当前结点只储存了下一个结点的位置,所以只能从头开始(遍历)查找任意结点的数据。
  • 对于循环链表,可以只声明一个尾指针 (tail),就可以找到头指针。所以可以快速地找到第一个和最后一个结点。但查找其他任意结点,仍然需要遍历。
  • 对于双向链表,因为当前结点同时储存了上一结点和下一结点的位置,所以可以从头开始查找任意结点,也可以从末尾开始查找任意结点,但仍然需要遍历。

综上,相对于数组直接用索引查找指定位置的数据,时间复杂度为 O(1)。链表只能遍历,时间复杂度为 O(N)。所以链表最大的缺点就是查找操作没有数组快。

三、如何操作链表

(一)单链表

Person类作为数据存放在链表的结点中。

/**
 * Person类,作为数据存放在链表的结点中
 *
 * @author likezhen
 * @version 1.0
 */
class Person {
    private String name;
    private int age;

    public Person() {
        super();
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

单向链表类中,声明了头指针 head 指向第一个结点,声明尾指针 tail 指向最后一个结点,并提供了增加、删除、遍历等操作。

/**
 * 单向链表类,提供了增加、删除、遍历等操作。
 *
 * @author likezhen
 * @version 1.0
 */
class SinglyLinkedList<E> {
    /**
     * 结点内部类。
     *
     * @author likezhen
     * @version 1.0
     */
    private static class Node<E> {
        private E element;
        private Node<E> next; //声明指向下一个结点的指针

        public Node(E e, Node<E> n) { //创建结点时必须声明其储存的数据和指针
            element = e; //当前结点存放的数据
            next = n; //当前结点指向下一个结点的指针
        }

        public void setNext(Node<E> n) {
            next = n;
        }

        public Node<E> getNext() { 
            return next;
        }

        public E getElement() {
            return element;
        }
    }

    private Node<E> head = null; //初始化头指针,头指针指向第一个结点
    private Node<E> tail = null; //初始化尾指针,尾指针指向最后一个结点
    private int size = 0; //初始化链表结点个数

    public SinglyLinkedList() {
        super();
    }

    public int getSize() {
        return size;
    }

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

    public void addFirst(E e) { //在表头插入一个结点
        head = new Node<>(e, head);
        if (size == 0)
            tail = head;
        size++;
    }

    public void addLast(E e) { //在表尾插入一个结点
        Node<E> newest = new Node<>(e, null);
        if (isEmpty())
            head = newest;
        else
            tail.setNext(newest);
        tail = newest;
        size++;
    }

    public E removeFirst() { //删除表头结点(第一个结点)
        if (isEmpty()) return null;
        E answer = head.getElement();
        head = head.getNext();
        size--;
        if (size == 0)
            tail = null;
        return answer;
    }

    public void displayLinkedList() {
        Node<E> current = head;
        while (current != null) {
            System.out.println(current.getElement().toString());
            current = current.getNext();
        }
    }
}

单向链表的测试类。

/**
 * 单向链表的测试类
 *
 * @author likezhen
 * @version 1.0
 */
public class SinglyLinkedListApp {
    public static void main(String[] args) {
        SinglyLinkedList<Person> singlyLinkedList = new SinglyLinkedList<>();
        singlyLinkedList.addFirst(new Person("Dane", 58)); //Dane所在的结点为第一个结点
        singlyLinkedList.addFirst(new Person("Jack", 25)); //Jack为第一个结点,Dane为第二个结点
        singlyLinkedList.addFirst(new Person("Tale", 16)); //Tale第一,Jack第二,Dane第三
        singlyLinkedList.addLast(new Person("Wily", 23)); //Wily为最后一个结点
        singlyLinkedList.addLast(new Person("Rien", 33)); //Rien为最后一个结点
        System.out.println("----------删除之前----------");
        singlyLinkedList.displayLinkedList();
        singlyLinkedList.removeFirst();
        System.out.println("----------删除之后----------");
        singlyLinkedList.displayLinkedList();
    }
}

测试结果:

D:\Java\jdk\bin\java.exe
----------删除之前----------
Person{name='Tale', age=16}
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
----------删除之后----------
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}

Process finished with exit code 

(二)循环链表

循环链表类中,声明尾指针 tail 指向最后一个结点,头指针可通过 head = tail.getNext()获得,所以无需声明和初始化头指针。提供了增加、删除、遍历等操作。

/**
 * 循环链表类,提供了增加、删除、遍历等操作。
 *
 * @author likezhen
 * @version 1.0
 */
class CircularlyLinkedList<E> {
    /**
     * 结点内部类。与单链表的结点类相同。
     * 
     * @author likezhen
     * @version 1.0
     */
    private static class Node<E> {
        private E element;
        private Node<E> next;

        public Node(E e, Node<E> n) {
            element = e;
            next = n;
        }

        public Node<E> getNext() {
            return next;
        }

        public void setNext(Node<E> next) {
            this.next = next;
        }

        public E getElement() {
            return element;
        }
    }
	//head = tail.getNext(); 所以无需特别地单独声明
    private Node<E> tail = null; //初始化尾指针,尾指针指向最后一个结点
    private int size = 0; //初始化链表的结点个数

    public CircularlyLinkedList() {
        super();
    }

    public int getSize() {
        return size;
    }

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

    public void rotate() { //将第一个结点变成最后一个结点
        if (tail != null)
            tail = tail.getNext();
    }

    public void addFirst(E e) { //在表头插入新结点
        if (size == 0) {
            tail = new Node<>(e, null);
            tail.setNext(tail);
        } else {
            Node<E> newest = new Node<>(e, tail.getNext());
            tail.setNext(newest);
        }
        size++;
    }

    public void addLast(E e) { //在表尾插入新结点
        addFirst(e);
        tail = tail.getNext();
    }

    public E removeFirst() { //删除表头结点(第一个结点)
        if (isEmpty()) return null;
        Node<E> head = tail.getNext();
        if (head == tail) tail = null;
        else tail.setNext(head.getNext());
        size--;
        return head.getElement();
    }

    public void displayLinkedList() {
        Node<E> head = tail.getNext();
        Node<E> current = head;
        do {
            System.out.println(current.getElement().toString());
            current = current.getNext();
        } while (current != head);
    }
}

循环链表的测试类。

/**
 * 循环链表的测试类
 *
 * @author likezhen
 * @version 1.0
 */
public class CircularlyLinkedListApp {
    public static void main(String[] args) {
        CircularlyLinkedList<Person> circularlyLinkedList = new CircularlyLinkedList<>();
        circularlyLinkedList.addFirst(new Person("Dane", 58)); 
        circularlyLinkedList.addFirst(new Person("Jack", 25));
        circularlyLinkedList.addFirst(new Person("Tale", 16));
        circularlyLinkedList.addLast(new Person("Wily", 23));
        circularlyLinkedList.addLast(new Person("Rien", 33));
        System.out.println("----------删除之前----------");
        circularlyLinkedList.displayLinkedList();
        circularlyLinkedList.removeFirst();
        System.out.println("----------删除之后----------");
        circularlyLinkedList.displayLinkedList();
        circularlyLinkedList.rotate(); //第一个结点Jack变成最后一个结点
        System.out.println("----------旋转之后----------");
        circularlyLinkedList.displayLinkedList();
    }
}

测试结果:

D:\Java\jdk\bin\java.exe com.linkedlist.www.CircularlyLinkedListApp
----------删除之前----------
Person{name='Tale', age=16}
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
----------删除之后----------
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
----------旋转之后----------
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
Person{name='Jack', age=25}

Process finished with exit code 0

(三)双向链表

相比于单链表和循环链表中的 Node 结点类,在双向链表的 Node 结点内部类中,需多声明指向前一个结点的指针 prev(用左指针可能更准确)。单链表、循环链表和双向链表中,都声明了指向后一个结点的指针 next

特殊地,在双向链表类中,可以通过声明不储存任何数据的空头节点 header 和空尾结点 trailer 来减少对于特殊情况(当链表为空)的判断,使得链表为空和链表不为空时的操作一致,减少判断语句。向链表中添加结点,即向 header 与 trailer 两个空结点之间添加新的结点。删除链表中的结点,即删除 header 与 trailer 两个空结点之间的结点。提供了增加、删除、遍历等操作。

/**
     * 双向链表类,提供了增加、删除、遍历等操作。
     *
     * @author likezhen
     * @version 1.0
     */
class DoublyLinkedList<E> {
    private static class Node<E> {
        private E element;
        private Node<E> prev; //声明左指针,指向前一个结点
        private Node<E> next; //声明右指针,指向后一个结点

        public Node(E e, Node<E> p, Node<E> n) {
            element = e;
            prev = p;
            next = n;
        }

        public E getElement() {
            return element;
        }

        public Node<E> getPrev() {
            return prev;
        }

        public void setPrev(Node<E> prev) {
            this.prev = prev;
        }

        public Node<E> getNext() {
            return next;
        }

        public void setNext(Node<E> next) {
            this.next = next;
        }
    }

    private Node<E> header; //声明头节点
    private Node<E> trailer; //声明尾结点
    private int size = 0; //初始化链表的结点个数
    
    public DoublyLinkedList() { //创建双向链表类时,创建并初始化头节点和尾结点。
        header = new Node<>(null, null, null); //创建并初始化头节点
        trailer = new Node<>(null, header, null); //创建并初始化尾节点,尾结点的左指针指向头节点
        header.setNext(trailer); //头节点的右指针指向尾结点,形成循环链表
    }

    public int getSize() {
        return size;
    }

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

    public void addFirst(E e) { //在表头插入新结点
        addBetween(e, header, header.getNext());
    }

    public void addLast(E e) { //在表尾插入新结点
        addBetween(e, trailer.getPrev(), trailer);
    }

    public E removeFirst() { //移除第二个结点(第一个结点为空头结点,不能移除)
        if (isEmpty()) return null;
        return remove(header.getNext());
    }

    public E removeLast() { //移除倒数第二个结点(最后一个结点为空尾结点,不能移除)
        if (isEmpty()) return null;
        return remove(trailer.getPrev());
    }

    public void displayLinkedList() {
        Node<E> current = header.getNext();
        while (current != trailer) {
            System.out.println(current.getElement().toString());
            current = current.getNext();
        }
    }

    private void addBetween(E e, Node<E> predecessor, Node<E> successor) {
        Node<E> newest = new Node<>(e, predecessor, successor);
        predecessor.setNext(newest);
        successor.setPrev(newest);
        size++;
    }

    private E remove(Node<E> node) {
        Node<E> predecessor = node.getPrev();
        Node<E> successor = node.getNext();
        predecessor.setNext(successor);
        successor.setPrev(predecessor);
        size--;
        return node.getElement();
    }
}

双向链表的测试类。

/**
 * 双向链表的测试类
 *
 * @author likezhen
 * @version 1.0
 */
public class DoublyLinkedListApp {
    public static void main(String[] args) {
        DoublyLinkedList<Person> doublyLinkedList = new DoublyLinkedList<>();
        doublyLinkedList.addFirst(new Person("Dane", 58));
        doublyLinkedList.addFirst(new Person("Jack", 25));
        doublyLinkedList.addFirst(new Person("Tale", 16));
        doublyLinkedList.addLast(new Person("Wily", 23));
        doublyLinkedList.addLast(new Person("Rien", 33));
        System.out.println("----------删除之前----------");
        doublyLinkedList.displayLinkedList();
        doublyLinkedList.removeFirst();
        System.out.println("----------删除之后----------");
        doublyLinkedList.displayLinkedList();
    }
}

测试结果:

D:\Java\jdk\bin\java.exe com.linkedlist.www.DoublyLinkedListApp
----------删除之前----------
Person{name='Tale', age=16}
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
----------删除之后----------
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}

Process finished with exit code 0

时间复杂度分析:
链表的删除、增加操作的时间复杂度都为常数级,即 O(1),与数据量无关。
链表的查找操作的最坏时间复杂度与数据量成线性,即 O(N)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值