线性表

242 阅读4分钟

线性表是一种最基本的数据结构,它是一个一维的有序元素序列。主要具有以下特征:

  1. 元素具有相同类型。线性表中存放的所有数据元素具有相同的数据类型。
  2. 元素具有线性顺序。线性表中的数据元素以线性的顺序依次排列,每个元素至多只有前驱和后继,不存在分支。
  3. 可以通过索引访问每个元素。线性表中每个元素都有其在表中的唯一位置,可以通过这个位置索引访问到该元素。
  4. 支持插入和删除操作。可以在任意位置插入和删除元素,且插入和删除不影响其他元素的相对位置。
  5. 大小可变。线性表的大小是动态变化的,没有固定大小的限制。

常见的线性表有:数组、字符串、链表、队列、栈等,下面我们来挨个介绍一下:


数组

数组主要特点如下:

  1. 存储元素连续:数组的每个元素都有相对定的位置,并且都是相邻存储的。这个特点使得数组支持随机访问,通过索引可以快速访问每个元素。
    int[] arr = new int[5];
    arr[0] = 1;
    arr[1] = 2;
    // arr[0] 和 arr[1] 存储在连续的内存空间
  1. 大小固定:数组一旦创建,大小就固定了。如果要添加更多元素,必须创建一个更大的新数组并复制原数组内容。这会导致内存占用变大和数据移动的时间损耗。
    int[] arr = new int[5];
    // 若要扩容至10,需要:
    int[] newArr = new int[10];
    for (int i = 0; i < 5; i++) {
        newArr[i] = arr[i];
    }
    arr = newArr;
  1. 插入和删除慢:由于数组的大小固定,插入和删除元素需要移动大量的数据,效率较低。要在中间插入或删除,需要移动所有后续元素。
    int[] arr = {1, 2, 3, 4, 5};
    // 在索引2插入6,需要:
    for (int i = 4; i >= 2; i--) {
    arr[i] = arr[i-1];
    }
    arr[2] = 6;
  1. 查找快:通过固定的索引可以快速获取任意位置的元素,时间复杂度为O(1)。
    int index = 3;
    int elem = arr[index];  // 直接访问索引为3的元素
  1. 优化内存占用:数组的空间是连续的,数组可以最大限度地利用CPU缓存,这有利于空间局部性原理,可以加速访问速度。
    // 快速排序实现  
    quicksort(arr, 0, arr.length - 1); 
  1. 支持快速排序:数组的随机访问特性,使得快速排序等需要随机访问的排序算法,更适合在数组上实现。

总之,数组具有存储密集和随机访问的特点,适合用于需要快速查找与排序的场景。但 Insert 和 Remove 等操作会因为数据移动而变慢。


字符串

字符串具有以下主要特点:

  1. 由字符序列构成:字符串是一串字符的有限序列。字符可以是字母、数字或其他符号。
    char[] chars = {'h', 'e', 'l', 'l', 'o'}; 
    String str = new String(chars);
  1. 具有长度:字符串有固定的字符数量,我们称为字符串的长度或字符串的大小。
    String str = "hello";
    int len = str.length();  // len为5
  1. 支持索引访问:可以通过索引访问字符串中的每个字符。
    String str = "hello";
    char c = str.charAt(2); // c为'l'
  1. 部分可变:字符串的长度一旦确定就不可变,但可以对字符串中的部分字符进行插入、删除和替换操作。
    String str = "hello";
    String sub = str.substring(1, 3);  // sub为"el"
    String newStr = str.replace('l', 'x'); // newStr为"hexxo"
  1. 无类型:字符串通常被实现为字符数组,而不是一种特定的数据类型。不同语言中字符串的实现细节不同。
  2. 支持遍历:可以从头到尾遍历字符串中的每个字符。
    String str = "hello";
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        // do something
    } 
  1. 支持子串:可以获取字符串的一部分,形成子串。子串也是字符串。
  2. 支持比较:可以比较两个字符串的字典序或长度,判断字符串是否相等。
    String a = "hello";  
    String b = "Hello";
    boolean isEqual = a.equals(b);  // isEqual为false,忽略大小写
  1. 支持拼接:可以将多个字符串拼接成一个字符串。
    String c = "hello" + "world";
    String d = "hello".concat("world");

字符串是一种非常基本和常用的数据结构,作为一名程序员,需要熟练掌握字符串的概念、特性和操作。


链表

链表具有以下主要特点:

  1. 元素无序:链表中的元素没有顺序,元素的逻辑顺序与物理顺序无关。
ListNode node1 = new ListNode(1);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(3);
node1.next = node2;  
node2.next = node3;
  1. 元素个数可变:链表的大小是动态变化的,没有固定大小限制。
while (true) {
    ListNode node = new ListNode(i++);
    node.next = head.next;
    head.next = node;
}
  1. 支持高效插入和删除:插入和删除操作只需要更改指针,时间复杂度为O(1),效率高。
// 插入节点 
ListNode node = new ListNode(4);
node.next = head.next;
head.next = node;

// 删除节点
head.next = head.next.next; 
  1. 没有索引:链表中的元素没有索引,无法通过索引快速访问元素。只能从头节点或尾节点开始逐个访问。
ListNode cur = head;
while (cur != null) {
    // 访问cur节点
    cur = cur.next;  
}
  1. 占用空间不连续:链表中的元素在内存中可以是不连续的,只需要存储元素本身和指针域。
ListNode node1 = new ListNode(1);  
ListNode node2 = new ListNode(2); 
node1.next = node2;  // 节点空间不连续,只通过指针连接
  1. 链表本身无限长度:链表的长度仅受内存大小的限制,没有理论上的限制。
  2. 支持增删改操作:支持对链表执行插入、删除和修改元素等操作。
// 增加节点  
ListNode newNode = new ListNode(4);
newNode.next = head.next;
head.next = newNode;   

// 删除节点
head.next = head.next.next;  

// 修改节点
head.next.val = 5; 

链表常见的分类有:

  1. 单链表:每个节点只包含指向后继节点的指针。
  2. 双向链表:每个节点包含指向前驱和后继节点的指针。
  3. 循环链表:尾节点的指针指向头节点,形成循环。

注意:链表的应用场景主要是插入和删除操作较频繁,且无序的场景。相比数组,链表的插入和删除操作更高效,但查找和访问元素较低效。


队列

队列(Queue)是一种遵循先入先出(FIFO)原则的有序集合。主要有以下特点:

  1. 只允许在一端插入元素,在另一端删除元素:队列有 head 和 tail 两个指针,新元素总是添加在 tail 所指向的位置,删除元素总是从 head 所指向的位置开始。
    Queue<Integer> queue = new LinkedList<>();
    queue.add(1);  // 尾部添加元素
    queue.add(2);
    queue.remove();  // 头部删除元素
  1. 先入先出:最早添加的元素最先删除,采用先进先出的顺序。
    queue.add(1);
    queue.add(2);
    queue.remove();  // 1被删除
    queue.remove();  // 2被删除
  1. 大小可变:队列的大小是动态的,没有固定大小的限制。
    // 一开始队列为空
    queue.add(1);  
    queue.add(2);  
    queue.remove();
    queue.add(3); 
  1. 实现简单:队列可以用数组或链表简单实现。使用数组实现时,需处理循环队列的情况。
    public class Queue<E> {
        private LinkedList<E> list;
        
        public void add(E element) {
            list.addLast(element);
        }
        
        public E remove() {
            return list.removeFirst();
        }
    }
  1. 适用于广度优先搜索:队列的FIFO特性很适合在广度优先搜索等层次遍历算法中使用。每一层的节点全部拓展完后才进行下一层的拓展。

    Queue<Integer> queue = new LinkedList<>(); 
    queue.add(source);  // 添加起点source
    while (!queue.isEmpty()) {
        int vertex = queue.remove();
        for (int neighbor : graph[vertex]) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                queue.add(neighbor);  // 新增节点添加至队尾
            }
        }
    }
  1. 适用于缓存:队列可以用来实现LRU(最近最少使用)缓存淘汰算法。Queue中元素时间久的排在前面,新添加元素排在后面。超出容量时,删除最前面的元素。

  2. 发展出更为抽象的数据结构:队列是一种较为简单的数据结构,但它衍生出更复杂的循环队列,优先队列等结构。


栈(Stack)是一种遵循后入先出(LIFO)原则的有序集合。主要有以下特点:

  1. 只允许在一端插入和删除元素:栈只有一个入口,元素只能从入口插入和删除,这使得栈的插入和删除操作非常高效,时间复杂度为O(1)。
    Stack<Integer> stack = new Stack<>();
    stack.push(1);  // 入栈
    stack.push(2);
    stack.pop();   // 出栈,2被弹出  
  1. 后入先出:最近添加的元素最先删除,采用后进先出的顺序。这种特性也被称为FILO(First In Last Out)。
    stack.push(1);  
    stack.push(2);
    stack.pop();  // 2被弹出  
    stack.pop();  // 1被弹出
  1. 大小可变:栈的大小是动态的,没有固定大小的限制。
    // 一开始栈为空
    stack.push(1);  
    stack.push(2);  // 大小变为2
    stack.pop();
    stack.push(3);   // 大小变为2 
  1. 实现简单:栈可以用数组或链表简单实现。使用数组实现时,维护一个指向栈顶的指针,插入和删除只操作数组的一端。
    public class Stack {
        private int[] arr;
        private int size;
        
        public void push(int value) {
            arr[size++] = value;  
        }
        
        public int pop() {
            return arr[--size];
        }
    }
  1. 适用于深度优先搜索:栈的FILO特性很适合在深度优先搜索等递归算法中使用。每次深入都将节点压入栈,回溯时从栈中弹出节点。
    void dfs(int vertex) {
        visited.add(vertex);
        for (int neighbor : graph[vertex]) {
            if (!visited.contains(neighbor)) 
                dfs(neighbor);   // 递归调用,相当于将节点压入栈
        }
    }
  1. 发展出更为抽象的数据结构:栈是一种较为简单的数据结构,但它衍生出很多其他的数据结构,比如队列、递归栈、平衡二叉树等。