数据结构与算法-线性表(下)链式存储结构(Java实例)

作者:逍遥Sean
简介:一个主修Java的Web网站\游戏服务器后端开发者
主页:https://blog.csdn.net/Ureliable
觉得博主文章不错的话,可以三连支持一下~ 如有需要我的支持,请私信或评论留言!

前言

算法非常重要,它是计算机科学的核心之一。算法是一组解决问题的步骤和规则,可以帮助我们在计算机程序中完成各种任务。好的算法可以优化程序的性能,提高程序的效率,并使程序更易于理解和维护。算法也是计算机科学中一种非常基础的概念,对于计算机科学专业的学生来说,学好算法将为他们日后的学习和工作奠定非常重要的基础。

上面的话是别人说的

有用没用不知道(可能潜移默化在开发中会受到这些思想影响),没事搞搞还是很有趣的~

本文写线性表的链式存储结构及其操作方法。
本文为Java代码实现,默认单链表结构为:

class ListNode {
    int val;
    ListNode next;
    ListNode(int val) {
        this.val = val;
    }
}

使用c++的玩家请移步:
数据结构与算法-线性表(下)链式存储结构(c++实例)

线性表(下)链式存储结构(Java实例)

  • 1 线性表链式存储结构简介
  • 2 单链表
    • 2.1 单链表的读取、插入与删除
    • 2.2 单链表的整表创建与删除
    • 3.3 单链表与顺序存储结构对比
  • 3 其他链表
    • 3.1 静态链表
      • 3.1.1 原理
      • 3.1.2 实例
    • 3.2 循环链表
      • 3.2.1 原理
      • 3.2.2 实例
    • 3.3 双向链表
      • 3.3.1 原理
      • 3.3.1 代码实例

1 线性表链式存储结构简介

线性表是一种数据结构,对数据元素进行逻辑上的顺序排列。线性表链式存储结构是指用链表来存储线性表的数据元素。链表由节点组成,每个节点包含两部分:数据域和指针域。数据域存储节点的数据元素,指针域存储指向下一个节点的指针。

链式存储结构相对于顺序存储结构的优点是可以更方便地进行插入和删除操作,通过修改节点的指针即可完成操作,而不需要移动大量数据。但是链式存储结构的缺点是访问节点需要通过指针进行跳转,相对于顺序存储结构,访问速度会慢一些。此外,链式存储结构需要额外的空间存储指针信息,会占用更多的内存空间。

链式存储结构广泛应用于各种算法和数据结构中,如链表、队列、栈等。

2 单链表

2.1 单链表的读取、插入与删除

单链表是一种经常使用的数据结构,它是由一系列“节点”组成的。每个节点包含两个属性:数据(或值)和指向下个节点的指针。单链表中只能从头到尾依次访问,不能从任意位置访问。

读取单链表中的节点值

要读取单链表中的节点值,需要从头节点开始,依次访问每个节点。具体代码如下:

ListNode head = ...; // 单链表头节点

while (head != null) {
    int val = head.val;
    System.out.println(val);
    head = head.next;
}

插入节点

在单链表中插入一个节点,需要先找到要插入位置的前一个节点,然后把新节点插入到这个节点的后面。具体代码如下:

public class LinkedList {
    Node head;

    static class Node {
        int data;
        Node next;

        Node(int d) {
            data = d;
            next = null;
        }
    }

    public void insertAtEnd(int data) {
        Node newNode = new Node(data);

        if (head == null) {
            head = newNode;
            return;
        }

        Node lastNode = head;
        while (lastNode.next != null) {
            lastNode = lastNode.next;
        }

        lastNode.next = newNode;
    }

    public void insertAfter(Node prevNode, int data) {
        if (prevNode == null) {
            System.out.println("Previous node cannot be null");
            return;
        }

        Node newNode = new Node(data);
        newNode.next = prevNode.next;
        prevNode.next = newNode;
    }

    public void printList() {
        Node currentNode = head;
        while (currentNode != null) {
            System.out.print(currentNode.data + " ");
            currentNode = currentNode.next;
        }
    }

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

        linkedList.insertAtEnd(1);
        linkedList.insertAtEnd(2);
        linkedList.insertAtEnd(4);
        linkedList.insertAfter(linkedList.head.next, 3);

        linkedList.printList();
    }
}

删除节点

在单链表中删除一个节点,需要先找到要删除的节点,然后把其前一个节点指向它后一个节点。具体代码如下:

class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

public class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        if (head == null) return null;
        if (head.val == val) return head.next;
        ListNode prev = head;
        ListNode curr = head.next;
        while (curr != null && curr.val != val) {
            prev = curr;
            curr = curr.next;
        }
        if (curr != null) {
            prev.next = curr.next;
        }
        return head;
    }
}

2.2 单链表的整表创建与删除

单链表是一种链式存储结构,每个节点包含一个数据元素和一个指向下一个节点的指针。单链表的整表创建方法如下:

1.声明一个头节点,不包含任何数据元素。
2.声明一个指向头节点的指针,即头指针。
3.逐个插入节点,每个节点的数据元素和指针由用户输入。
4.最后一个节点的指针指向 NULL,表示链表结束。

以下是单链表的整表创建与删除的代码示例:

  1. 定义单链表节点类Node:
class Node {
    public int data;
    public Node next;

    public Node(int data) {
        this.data = data;
        this.next = null;
    }
}
  1. 定义单链表类LinkedList:
class LinkedList {
    public Node head;

    public LinkedList() {
        this.head = null;
    }

    // 创建链表
    public void createLinkedList(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        this.head = new Node(arr[0]);
        Node curNode = this.head;

        for (int i = 1; i < arr.length; i++) {
            Node newNode = new Node(arr[i]);
            curNode.next = newNode;
            curNode = curNode.next;
        }
    }

    // 删除链表
    public void deleteLinkedList() {
        this.head = null;
    }

    // 打印链表
    public void printLinkedList() {
        Node curNode = this.head;

        while (curNode != null) {
            System.out.print(curNode.data + " ");
            curNode = curNode.next;
        }

        System.out.println();
    }
}
  1. 在主函数中测试链表的创建与删除:
public static void main(String[] args) {
    // 创建链表
    LinkedList linkedList = new LinkedList();
    int[] arr = new int[]{1, 2, 3, 4, 5};
    linkedList.createLinkedList(arr);
    linkedList.printLinkedList();

    // 删除链表
    linkedList.deleteLinkedList();
    linkedList.printLinkedList();
}

输出结果为:

1 2 3 4 5 

以上是单链表的整表创建和删除操作的代码示例,可以根据实际需要进行修改。

3.3 单链表与顺序存储结构对比

单链表和顺序存储结构都是线性结构,但它们在实现方式和性能方面有很大的不同。

  1. 实现方式
    单链表使用指针来连接每个节点,每个节点只有一个指针域,它指向下一个节点或者为空。而顺序存储结构使用连续的存储空间来存储数据元素,可以通过数组下标来访问每个元素。

  2. 插入和删除操作
    单链表在插入和删除节点时,只需要修改指针域即可,不需要移动其他节点。这使得单链表的插入和删除操作非常高效。而顺序存储结构在插入和删除元素时需要移动其他元素,因此在大量插入和删除操作时性能较差。

  3. 随机访问性能
    顺序存储结构支持随机访问,可以通过数组下标来访问任何一个元素。而单链表不支持随机访问,只能从头开始遍历整个链表来访问某个节点。

  4. 存储空间利用率
    单链表每个节点只需要存储一个指针域和一个数据域,不需要预留连续空间,因此存储空间利用率较高。而顺序存储结构需要预留连续空间,因此存储空间利用率较低。

综上所述,单链表适合频繁插入和删除操作、不需要随机访问、对存储空间利用率要求较高的场合。而顺序存储结构适合需要随机访问、对存储空间利用率要求不高的场合。

3 其他链表

除了单链表,还有以下其他常见的链表:

  1. 双向链表(Doubly Linked List):每个节点包含两个指针,一个指向前一个节点,一个指向后一个节点。双向链表可以从前往后或从后往前遍历。
  2. 循环链表(Circular Linked List):最后一个节点指向第一个节点,形成一个环形结构。循环链表可以从任意一个节点开始遍历。
  3. 静态链表(Static Linked List):使用数组的方式实现链表,每个节点不再使用指针指向下一个节点,而是使用数组索引指向下一个节点。
  4. 多级链表(Multilevel Linked List):每个节点除了指向下一个节点的指针,还有指向另一个链表的指针。这种链表可以用来实现树形结构。
  5. 线索二叉树(Threaded Binary Tree):二叉树的叶子节点的左右指针指向中序遍历下的前驱和后继节点,可以加速中序遍历的过程。

这些链表的使用场景各不相同,需要根据具体问题场景选择合适的链表。

3.1 静态链表

3.1.1 原理

静态链表是一种使用数组模拟链表的数据结构,它不同于普通链表,它使用数组来存储结点,每个结点都包含两个信息:数据域和指针域。指针域实际上存储的是下一个结点在数组中的索引,这样可以避免指针操作的开销和内存分配的问题。静态链表在插入和删除时,需要对数组进行修改和移动,因此可能导致效率低下。但是,它的一个优点是可以随意访问链表中的任何元素,而不需要从头开始遍历链表。它还可以在一些存储空间受限的场合下使用。
静态链表是一种使用数组来实现链表的数据结构,其特点是空间固定,元素大小不变。静态链表的操作包括:

  1. 初始化:定义一个数组作为静态链表,将所有元素的指针值初始化为-1。
  2. 插入操作:在静态链表中插入一个元素,需要先找到一个空闲的元素位置,可以使用一个指针记录空闲的位置。然后将要插入的元素的数据和指针值存储在这个空闲位置上,将该位置的指针值更新为插入位置的指针值,再将插入位置的指针值更新为新元素的下标。
  3. 删除操作:删除静态链表中的一个元素,需要将要删除的元素的指针值赋值给其前驱元素的指针值,然后将要删除的元素的位置作为空闲位置。
  4. 查找操作:静态链表的查找操作与普通链表相同,需要从头节点开始遍历,找到指定元素。
  5. 遍历操作:可以使用一个指针来记录当前访问的元素的位置,从头节点开始遍历整个链表。
  6. 修改操作:静态链表的修改操作与普通链表相同,需要找到指定元素,并修改其数据。
  7. 销毁操作:销毁静态链表需要释放数组的内存空间。

3.1.2 实例

以下是一个静态链表的代码实例。静态链表是使用数组实现的链表。

public class StaticLinkedList {
    private Node[] data; // 存储节点的数组
    private int size; // 链表大小
    private int free; // 空闲节点索引

    private static class Node {
        int data;
        int next;

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

    public StaticLinkedList(int maxSize) {
        data = new Node[maxSize];
        for (int i = 0; i < maxSize - 1; i++) {
            data[i] = new Node(0, i + 1); // 初始化每个节点的下一个索引
        }
        data[maxSize - 1] = new Node(0, -1); // 最后一个节点的下一个索引为-1
        size = 0;
        free = 0;
    }

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

    public boolean isFull() {
        return free == -1;
    }

    public int size() {
        return size;
    }

    public boolean add(int value) {
        if (isFull()) {
            return false;
        }
        Node node = data[free];
        free = node.next; // 找到新的空闲节点
        node.data = value;
        node.next = -1;
        if (isEmpty()) {
            data[0] = node; // 链表为空时插入第一个节点
        } else {
            Node tail = data[size - 1];
            tail.next = free; // 将新节点插入链表尾部
            data[size] = node;
        }
        size++;
        return true;
    }

    public boolean remove(int value) {
        int index = 0;
        for (int i = 0; i < size; i++) {
            Node node = data[i];
            if (node.data == value) {
                if (i == 0) {
                    data[0] = data[1]; // 删除头节点
                } else {
                    Node prev = data[index - 1];
                    prev.next = node.next; // 删除中间节点
                }
                node.next = free;
                free = i;
                size--;
                return true;
            }
            index = i;
        }
        return false;
    }

    public void print() {
        if (isEmpty()) {
            System.out.println("[]");
        } else {
            StringBuilder sb = new StringBuilder("[");
            Node node = data[0];
            while (node != null) {
                sb.append(node.data);
                node = node.next != -1 ? data[node.next] : null;
                if (node != null) {
                    sb.append(", ");
                }
            }
            sb.append("]");
            System.out.println(sb.toString());
        }
    }
}

StaticLinkedList list = new StaticLinkedList(5);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.print(); // [1, 2, 3, 4, 5]
list.add(6);
list.print(); // [1, 2, 3, 4, 5]
list.remove(3);
list.print(); // [1, 2, 4, 5]

3.2 循环链表

3.2.1 原理

循环链表与普通链表最大的区别在于其最后一个节点指向链表的第一个节点,形成一个循环。
这种数据结构可以更容易地处理需要访问链表中所有元素的任务,因为访问最后一个元素后可以直接访问第一个元素,而不需要从头开始访问。在实现循环队列和循环缓冲区时,循环链表也经常被使用。使用循环链表可以减少代码的复杂性,提高代码的效率。
循环链表与单链表或双向链表的操作类似,但需要特别注意处理尾部指针的问题。
常规操作包括:

  1. 创建循环链表:类似于创建单链表或双向链表,需要初始化头结点,并将其尾部指针指向自身。
  2. 插入节点:分为插入头结点和插入非头结点两种情况。插入头结点时,需要将尾部指针指向新的头结点;插入非头结点时,需要将新节点的next指向原节点的next,原节点的next指向新节点。
  3. 删除节点:同样分为删除头结点和删除非头结点两种情况。删除头结点时,需要将尾部指针指向新的头结点;删除非头结点时,需要将要删除节点的前一个节点的next指向要删除节点的后一个节点。
  4. 查找节点:从头结点开始遍历链表,直到找到目标节点。
  5. 遍历循环链表:从头结点开始遍历,直到再次回到头结点为止。

需要注意的是,在处理循环链表时,需要注意头结点和尾部指针的更新,否则容易死循环或遗漏节点。

3.2.2 实例

以下是一个简单的循环链表 Java 实例,其中链表节点包括数据和指向下一个节点的指针。

public class CircularLinkedList {

    // 节点类
    private static class Node {
        int data;
        Node next;

        Node(int data) {
            this.data = data;
        }
    }

    private Node tail;  // 尾节点

    // 检查链表是否为空
    public boolean isEmpty() {
        return tail == null;
    }

    // 返回链表长度
    public int size() {
        if (isEmpty()) {
            return 0;
        }

        Node current = tail.next;
        int count = 1;
        while (current != tail) {
            count++;
            current = current.next;
        }
        return count;
    }

    // 在链表头部插入节点
    public void addFirst(int data) {
        Node node = new Node(data);

        if (isEmpty()) {
            tail = node;
            tail.next = tail;
        } else {
            node.next = tail.next;
            tail.next = node;
        }
    }

    // 在链表末尾插入节点
    public void addLast(int data) {
        addFirst(data);
        tail = tail.next;
    }

    // 从链表头部删除节点
    public int removeFirst() {
        if (isEmpty()) {
            throw new RuntimeException("链表为空");
        }

        int data = tail.next.data;

        if (tail == tail.next) {
            tail = null;
        } else {
            tail.next = tail.next.next;
        }

        return data;
    }

    // 打印链表元素
    public void printList() {
        if (isEmpty()) {
            System.out.println("链表为空");
            return;
        }

        Node current = tail.next;
        do {
            System.out.print(current.data + " ");
            current = current.next;
        } while (current != tail.next);
        System.out.println();
    }
}

可以使用以下代码进行测试:

public class Main {
    public static void main(String[] args) {
        CircularLinkedList list = new CircularLinkedList();

        list.addLast(1);
        list.addLast(2);
        list.addLast(3);
        list.addLast(4);

        System.out.println("链表元素个数:" + list.size());
        list.printList();

        list.addFirst(0);
        list.addLast(5);

        System.out.println("链表元素个数:" + list.size());
        list.printList();

        System.out.println("删除头部元素:" + list.removeFirst());
        System.out.println("链表元素个数:" + list.size());
        list.printList();
    }
}

输出结果为:

链表元素个数:4
1 2 3 4 
链表元素个数:6
0 1 2 3 4 5 
删除头部元素:0
链表元素个数:5
1 2 3 4 5 

3.3 双向链表

3.3.1 原理

双向链表是一种特殊链表,每个节点包含两个指针,一个指向前一个节点,一个指向后一个节点。以下是双向链表的常用操作:

  1. 插入:在链表的任意位置插入一个节点,可以分为头部插入、尾部插入和中间插入三种情况。
  2. 删除:删除链表中的一个节点,同样可以分为头部删除、尾部删除和中间删除三种情况。
  3. 遍历:访问链表中的每个节点,可以从头到尾或从尾到头遍历,也可以在指定位置开始遍历。
  4. 查找:在链表中查找指定的值或目标节点,可以从头到尾或从尾到头查找,也可以在指定位置开始查找。
  5. 修改:修改链表中的节点的值或指针指向。

以上是双向链表的基本操作,还有一些其他常用操作,比如反转链表、合并链表等,都是基于上述基本操作实现的。

3.3.1 代码实例

下面是一个简单的双向链表 Java 实现:

public class DoublyLinkedList<E> {
    private int size;
    private Node<E> head;
    private Node<E> tail;

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

    public void addFirst(E data) {
        Node<E> newNode = new Node<>(data);
        if (isEmpty()) {
            tail = newNode;
        } else {
            head.setPrev(newNode);
        }
        newNode.setNext(head);
        head = newNode;
        size++;
    }

    public void addLast(E data) {
        Node<E> newNode = new Node<>(data);
        if (isEmpty()) {
            head = newNode;
        } else {
            tail.setNext(newNode);
            newNode.setPrev(tail);
        }
        tail = newNode;
        size++;
    }

    public boolean add(int index, E data) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException();
        }
        if (index == 0) {
            addFirst(data);
        } else if (index == size) {
            addLast(data);
        } else {
            Node<E> currentNode = getNode(index);
            Node<E> newNode = new Node<>(data);
            newNode.setPrev(currentNode.getPrev());
            newNode.setNext(currentNode);
            currentNode.getPrev().setNext(newNode);
            currentNode.setPrev(newNode);
            size++;
        }
        return true;
    }

    public Node<E> removeFirst() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        Node<E> removedNode = head;
        if (size == 1) {
            head = null;
            tail = null;
        } else {
            head = removedNode.getNext();
            head.setPrev(null);
        }
        size--;
        return removedNode;
    }

    public Node<E> removeLast() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        Node<E> removedNode = tail;
        if (size == 1) {
            head = null;
            tail = null;
        } else {
            tail = removedNode.getPrev();
            tail.setNext(null);
        }
        size--;
        return removedNode;
    }

    public Node<E> remove(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException();
        }
        if (index == 0) {
            return removeFirst();
        } else if (index == size - 1) {
            return removeLast();
        } else {
            Node<E> removedNode = getNode(index);
            removedNode.getPrev().setNext(removedNode.getNext());
            removedNode.getNext().setPrev(removedNode.getPrev());
            size--;
            return removedNode;
        }
    }

    public E get(int index) {
        return getNode(index).getData();
    }

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

    public int size() {
        return size;
    }

    private Node<E> getNode(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException();
        }
        Node<E> currentNode = head;
        for (int i = 0; i < index; i++) {
            currentNode = currentNode.getNext();
        }
        return currentNode;
    }

    private static class Node<E> {
        private E data;
        private Node<E> prev;
        private Node<E> next;

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

        public E getData() {
            return data;
        }

        public void setData(E data) {
            this.data = data;
        }

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

这个实现中,我们定义了 DoublyLinkedList 类,它包含一个内部类 Node,它表示链表的节点。每个节点都保存一个 data 值,以及指向前一个节点和后一个节点的引用。双向链表中每个节点都有一个前一个节点和后一个节点,这样可以在链表中向前或向后遍历。我们也定义了 sizeheadtail 类变量,size 表示链表大小,headtail 分别表示头节点和尾节点。如果链表为空,headtail 应该为 null

DoublyLinkedList 类包含下面这些方法:

  • addFirst(data):在链表的开头添加一个新节点,将 data 作为新节点的 data 值。如果链表为空,新节点将成为唯一的节点,同时将 tail 设为新节点;如果不为空,将现有的头节点 head 的前一个节点指向新节点,并将新节点的后一个节点指向 head,最后将 head 设为新节点。
  • addLast(data):在链表的末尾添加一个新节点,将 data 作为新节点的 data 值。如果链表为空,新节点将成为唯一的节点,同时将 head 设为新节点;如果不为空,将现有的尾节点 tail 的后一个节点指向新节点,并将新节点的前一个节点指向 tail,最后将 tail 设为新节点。
  • add(index, data):在链表的指定位置 index 添加一个新节点,将 data 作为新节点的 data 值。如果 index 在链表的范围之外,则引发 IndexOutOfBoundsException 异常。如果 index 等于 0,则调用 addFirst 方法;如果 index 等于 size,则调用 addLast 方法;否则,查找位置为 index 的节点并将新节点插入节点列表中。
  • removeFirst():从链表的开头删除头节点,并返回被删除的节点。如果链表为空,引发 NoSuchElementException 异常。如果链表不为空,将 head 的值设为 head 的下一个节点,并将 head 的前一个节点设为 null(如果存在)。最后,将链表大小 size 减 1。
  • removeLast():从链表的末尾删除尾节点,并返回被删除的节点。如果链表为空,引发 NoSuchElementException 异常。如果链表不为空,将 tail 的值设为 tail 的前一个节点,并将 tail 的后一个节点设为 null(如果存在)。最后,将链表大小 size 减 1。
  • remove(index):从链表的指定位置 index 删除节点,并返回被删除的节点。如果 index 在链表的范围之外,则引发 IndexOutOfBoundsException 异常。如果 index 等于 0,则调用 removeFirst 方法;如果 index 等于 size - 1,则调用 removeLast 方法;否则,查找位置为 index 的节点并将其从链表中删除。
  • get(index):返回链表中位置为 index 的节点的 data 值。如果 index 在链表的范围之外,则引发 IndexOutOfBoundsException 异常。
  • isEmpty():如果链表为空,则返回 true;否则返回 false。
  • size():返回链表的大小。

DoublyLinkedList 类中,我们还定义了一个私有方法 getNode(index),它返回链表中位置为 index 的节点。该方法用于实现 addremoveget 方法。如果 index 在链表的范围之外,则引发 IndexOutOfBoundsException 异常。在 Node 内部类中,我们定义了每个节点的 data 值、前一个节点和后一个节点。因为这些字段都是私有的,我们还定义了 getDatasetDatagetPrevsetPrevgetNextsetNext 方法以访问它们。

你可能感兴趣的:(数据结构和算法,算法,数据结构,java)