3.最基础的动态数据结构:链表

一. 什么是链表

我们以前博文中已经实现的线性数据结构:


链表和之前的数据结构不同, 它是真正的动态数据结构

1.链表的特点:

- 真正的动态数据结构
- 最简单的动态数据结构
- 可以更深入的理解引用(或指针)
- 可以更深入的理解递归
- 辅助组成其他数据结构

2.链表的描述

- 数据存储在“节点”(Node)
- 优点:真正的动态,不需要处理固定容量的问题
- 缺点:丧失了随机访问的能力(无法索引)

3. 数组和链表的对比

  • 数组
- 最好用于索引有语义的情况
- 最大优点:支持快速查询
  • 链表
- 不适合索引有语义的情况
- 最大优点:动态

4.代码架构搭建

新建项目LinkedList, src中新建文件LinkedList.java:

public class LinkedList<E> {
    private class Node{
        public E e;
        public Node next;

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

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

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

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


二. 在链表中添加元素

1.在表头添加元素

image

2.在中间插入元素

image

顺序很重要,如果顺序颠倒就出错, 必须

 node.next = prev.next
 prev.next = node

代码实现

LinkedList.java

public class LinkedList<E> {
    private class Node {···}


    private Node head;
    int size;

    public LinkedList() {
        head = null;
        size = 0;
    }

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

    // 返回链表是否为空
    public boolean isEmpty() {
        return size == 0;
    }

    // 在链表头添加新的元素e
    public void addFirst(E e) {
//        Node node = new Node(e);
//        node.next = head;
//        head = node

        head = new Node(e, head);  //一句话代表了三句话的意思
        size++;
    }


    // 在链表的index(0-based)位置添加新的元素e
    // 在链表中不是一个常用的操作, 练习用
    public void add(int index, E e) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed. Illegal index");
        }

        if (index == 0) {    //index=0时需要特殊处理
            addFirst(e);
        } else {
            Node pre = head;
            for (int i = 0; i < index - 1; i++) {
                prev = prev.next;
            }

//            Node node = new Node(e);
//            node.next = prev.next;
//            prev.next = node;
            prev.next = new Node(e, prev.next);

            size++;
        }

    }
    
    // 在链表末尾添加新的元素
    public void addLast(E e){
        add(size, e);
    }
    
}


三. 使用链表的虚拟头节点(dummyHead)

为了逻辑上的方便,设立虚拟头节点dummyHead. image

这样所有的节点都有前节点了。
修改add方法:

    public void add(int index, E e) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed. Illegal index");
        }

        // 不需要对index=0的情况特殊处理了
        Node prev = dummyHead;
        for (int i = 0; i < index; i++) {  //i<index-1改为i<index
            prev = prev.next;
        }

//            Node node = new Node(e);
//            node.next = prev.next;
//            prev.next = node;
        prev.next = new Node(e, prev.next);

        size++;
        

    }


    // 在链表头添加新的元素e
    public void addFirst(E e) {
//        head = new Node(e, head);  
//        size++;
        add(0, e);
    }

    // 在链表末尾添加新的元素
    public void addLast(E e){
        add(size, e);
    }

四. 链表的遍历,查询和修改

在LinkedList中编写相关代码

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

    public E getFirst(){
        return get(0);
    }

    public E getLast(){
        return get(size-1);
    }

    //修改链表第index个位置的元素
    public void set(int index, E e){
        if(index < 0 || index>=size){
            throw new IllegalArgumentException("Set failed. Illegal index.");
        }

        Node cur = dummyHead.next;
        for(int i = 0; i < index; i++){
            cur = cur.next;
        }
        cur.e = e;
    }

    // 查找链表中是否有元素e
    public boolean contains(E e){
        Node cur = dummyHead.next;
        while(cur != null){    //cur!=null表示cur为有效节点
            if(cur.e.equals(e)){
                return true;
            }
            cur = cur.next;
        }

        return false;
    }

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

        Node cur = dummyHead.next;
        while(cur != null){
            res.append(cur+"->");
            cur = cur.next;
        }

        res.append("null");
        return res.toString();
    }


在Main.java中编写对应的测试代码

public class Main {

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

        linkedList.add(2, 666);
        System.out.println(linkedList);
    }
}

运行结果:

0->null
1->0->null
2->1->0->null
3->2->1->0->null
4->3->2->1->0->null
4->3->666->2->1->0->null


五. 从链表中删除元素

1. 原理:

image

2. 在LinkedList中编写删除操作的代码

 // 从链表删除元素,返回被删除的元素
    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Remove failed.Illegal index.");
        }

        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.e;
    }

    // 从链表中删除第一个元素, 返回被删除元素
    public E removeFirst() {
        return remove(0);
    }


    // 从链表中删除最后一个元素, 返回被删除元素
    public E removeLast() {
        return remove(size - 1);
    }

3. 链表的时间复杂度分析

  • 添加操作O(n)
addLast(e)         O(n)
addFirst(e)        O(1)
add(index, e)      O(n/2) = O(n) 
  • 删除操作O(n)
removeLast(e)      O(n)
removeFirst(e)     O(1)
remove(index, e)   O(n/2) = O(n) 
  • 修改操作O(n)
set(index, e)      O(n)
  • 查找操作O(n)
get(index)         O(n)
contains(e)        O(n)

得出结论

如果只对链表头进行操作:O(1)
只查链表头的元素:O(1)

因为这样的特性,我们可以利用链表实现栈


六. 使用链表实现栈

接口文件Stack.java

public interface Stack<E> {
    int getSize();
    boolean isEmpty();
    void push(E e);
    E pop();
    E peek();
}

基于链表实现栈LinkedListStack.java

public class LinkedListStack<E> implements Stack<E> {
    private LinkedList<E> list;

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


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

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

    @Override
    public void push(E e){
        list.addFirst(e);
    }

    @Override
    public E pop(){
        return list.removeFirst();
    }

    @Override
    public E peek(){
        return list.getFirst();
    }


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

        res.append("Stack: top ");
        res.append(list);
        return res.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);
    }
}

运行结果:

Stack: top 0->null
Stack: top 1->0->null
Stack: top 2->1->0->null
Stack: top 3->2->1->0->null
Stack: top 4->3->2->1->0->null
Stack: top 3->2->1->0->null

对ArrayStack和LinkedListStack进行效率对比
Main.java

import java.util.Random;

public class Main {

    private static double testQueue(Stack<Integer> q, int opCount) {
        long startTime = System.nanoTime();

        Random random = new Random();
        for (int i = 0; i < opCount; i++) {
            q.push(random.nextInt(Integer.MAX_VALUE));
        }
        for (int i = 0; i < opCount; i++) {
            q.pop();
        }

        long endTime = System.nanoTime();

        return (endTime - startTime) / 1000000000.0; // 纳秒转为秒
    }


    public static void main(String[] args) {
        int opCount = 100000;

        // ArrayStackresize中消耗很多时间
        ArrayStack<Integer> arraystack = new ArrayStack<>();
        double time1 = testQueue(arraystack, opCount);
        System.out.println("ArrayStack, time: " + time1);

        // LinkedListStack在不停地new Node的过程中消耗很多时间
        LinkedListStack<Integer> linkedliststack = new LinkedListStack<>();
        double time2 = testQueue(linkedliststack, opCount);
        System.out.println("LinkedListStack, time: " + time2);
    }


}

运行结果

ArrayStack, time: 0.032472117
LinkedListStack, time: 0.02170341

将opCount变为10000000

ArrayStack, time: 1.48001151
LinkedListStack, time: 3.457722535
  • ArrayStack在resize中消耗很多时间
  • LinkedListStack在不停地new Node的过程中消耗很多时间
  • 因为时间复杂度相同, 所以两者效率差不多。

七. 带有尾指针的链表: 使用链表实现队列

1. 改进我们的链表

image

2. 代码实现

在队列的项目Queue中新建LinkedListQueue.java

public class LinkedListQueue<E> implements Queue<E> {

    private class Node {
        private E e;
        public Node next;

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

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

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

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

    private Node head, tail;
    private int size;

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

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

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

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

        size++;
    }

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

        Node retNode = head;
        head = head.next;
        retNode.next = null;

        if (head == null) {
            tail = null;
        }

        size--;
        return retNode.e;
    }

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

        return head.e;
    }

    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        res.append("Queue front ");

        Node cur = head;
        while(cur != null){
            res.append(cur + "->");
            cur = cur.next;
        }

        res.append("NULL tail");
        return res.toString();
    }

    //测试
    public static void main(String[] args) {
        LinkedListQueue<Integer> queue = new LinkedListQueue<>();

        for (int i = 0; i < 10; i++) {
            queue.enqueue(i);
            System.out.println(queue);

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

运行得到测试结果:

Queue front 0->NULL tail
Queue front 0->1->NULL tail
Queue front 0->1->2->NULL tail
Queue front 1->2->NULL tail
Queue front 1->2->3->NULL tail
Queue front 1->2->3->4->NULL tail
Queue front 1->2->3->4->5->NULL tail
Queue front 2->3->4->5->NULL tail
Queue front 2->3->4->5->6->NULL tail
Queue front 2->3->4->5->6->7->NULL tail
Queue front 2->3->4->5->6->7->8->NULL tail
Queue front 3->4->5->6->7->8->NULL tail
Queue front 3->4->5->6->7->8->9->NULL tail

3.在Main.java中比较一下三种Queue(ArrayQueue, LoopQueue, LinkedListQueue)的效率:

Main.java

import java.util.Random;

public class Main {
    private static double testQueue(Queue<Integer> q, int opCount) {
        long startTime = System.nanoTime();

        Random random = new Random();
        for (int i = 0; i < opCount; i++) {
            q.enqueue(random.nextInt(Integer.MAX_VALUE));
        }
        for (int i = 0; i < opCount; i++) {
            q.dequeue();
        }

        long endTime = System.nanoTime();

        return (endTime - startTime) / 1000000000.0; // 纳秒转为秒
    }


    public static void main(String[] args) {
        int opCount = 100000;

        ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
        double time1 = testQueue(arrayQueue, opCount);
        System.out.println("ArrayQueue, time: "+time1);

        LoopQueue<Integer> loopQueue = new LoopQueue<>();
        double time2 = testQueue(loopQueue, opCount);
        System.out.println("LoopQueue, time: "+time2);

        LinkedListQueue<Integer> linkedListQueue = new LinkedListQueue<>();
        double time3 = testQueue(linkedListQueue, opCount);
        System.out.println("LinkedListQueue, time: "+time3);
    }
}

运行结果:

ArrayQueue, time: 43.617662698
LoopQueue, time: 0.019108906
LinkedListQueue, time: 0.010479048



猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/80866952
今日推荐