1.数据结构的本质

数据结构的本质其实就是链表和数组,其他复杂的数据结构不过是这些基本结构的高级抽象。无论是树、栈、队列,还是更高级的图结构,归根结底都是基于数组或链表构建的。它们通过底层的实现细节和不同的逻辑组织方式,提供了更易用的API接口。

DataStructure

比如,这种数据结构,既可以用数组来实现,也可以用链表构建。数组的优势在于可以通过索引快速访问,节省空间,而链表的优势则在于高效的插入和删除操作。但链表由于使用指针,会占用更多的空间。

我们可以从Redis的实现上看到这一点,它内部有字符串、集合等数据结构,每一种数据类型都有不同的实现方式。其实,我们自己也可以根据具体的业务场景,设计并实现出个性化的数据结构,并定义一些适合当前业务需求的API接口。

语言层面也为我们提供了丰富的数据结构工具。比如在Python中,除了常用的字典和列表外,还可以通过collections模块找到一些更灵活和高效的数据类型,如defaultdict、双端队列deque、计数器Counter等。Javajava.util包中也包含了大量用于扩展基础数据结构的工具类。

综上所述,数据结构的底层其实就是数组和链表。无论是集合还是字典,都是通过哈希函数来将元素映射到具体的存储位置上。

那么,算法的本质是什么?

算法的本质其实就是穷举。但穷举并不是那么简单的,它需要做到两点:不漏聪明的穷举

计算机与人类解决问题的思维方式完全不同。人类可能通过一个公式就能解决问题,而计算机则依赖计算能力不断进行尝试,直到得到结果。计算机最大的优点就是速度快、不怕累。因此,算法就是用计算机语言把人的问题转化为机器能够执行的程序,通过不断的尝试来解决。

1. 不漏

“不漏”指的是算法给出的解答必须是完整的。比如很多组合问题常用回溯算法来解决,算法需要确保所有可能的解都被考虑到,没有遗漏。

2. 聪明的穷举

“聪明的穷举”就是通过优化策略来提高效率。最简单的算法可能是暴力破解,但它通常效率低下。为了提高效率,我们需要进行优化,而所有的优化手段,归根结底都是为了更聪明地穷举

举个例子,双指针滑动窗口技巧就是为了在穷举过程中减少不必要的时间消耗。相比于传统的双重循环,它们能显著提高时间效率。同样的,有时可以选择更合适的数据结构来减少穷举,比如使用双链表代替单链表,可以节省一定的操作步骤。

在算法设计中,还有一些优化技巧,比如剪枝,即在遍历树时提早终止不必要的分支;又比如记忆化搜索,通过缓存中间结果来避免重复计算。

综上所述,数据结构其实就是在说程序存储和读取数据的方法,算法则是在说计算方法不同导致的资源占用和效率不同

2. 线性数据结构

数组 (Array)
  • 定义与特性
    数组是内存中连续分配的元素集合,支持常数时间的随机访问。
    1
    2
    3
    int arr[5] = {1, 2, 3, 4, 5};  // 定义一个包含5个整数的数组
    int element = arr[2]; // 访问索引为2的元素
    arr[1] = 10; // 修改索引为1的元素
  • 优缺点分析
    • 优点:支持常数时间的随机访问,适合查找场景。
    • 缺点:插入和删除操作需要移动大量元素,效率较低。
  • 典型应用场景与变体
    • 动态数组:大小可动态变化的数组,如C++中的std::vector
    • 稀疏矩阵:存储少量非零元素的矩阵,节省内存。
链表 (Linked List)
  • 定义与特性
    链表是由一系列节点组成的,每个节点包含数据和指向下一个节点的指针。支持高效的插入和删除操作。
    1
    2
    3
    4
    5
    6
    7
    8
    struct Node {
    int data;
    Node* next;
    };

    Node* head = nullptr; // 创建一个空链表
    Node* newNode = new Node{10, nullptr}; // 创建一个新节点
    head = newNode; // 将新节点作为链表头部
  • 单链表与双链表
    • 单链表:每个节点仅指向下一个节点。
    • 双链表:每个节点同时指向前一个和后一个节点,便于双向遍历。
  • 优缺点分析
    • 优点:插入和删除操作的时间复杂度为O(1)。
    • 缺点:不能随机访问,查找某个元素需要遍历链表。
  • 典型应用
    • LRU缓存:使用双向链表和哈希表实现最近最少使用(Least Recently Used)缓存策略。
    • 队列:使用链表实现的队列,支持高效的入队和出队操作。
栈与队列
  • 栈 (Stack)

    • 定义与特性
      栈是一种后进先出(LIFO, Last In First Out)数据结构,只有栈顶元素可以被访问。常用于递归问题的处理。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      const int MAX = 1000;
      int stack[MAX];
      int top = -1;

      void push(int x) {
      stack[++top] = x;
      }

      int pop() {
      return stack[top--];
      }
    • 实际应用
      • 递归求解:如深度优先搜索(DFS)中使用栈模拟递归。
      • 表达式求值:后缀表达式求值。
  • 队列 (Queue)

    • 定义与特性
      队列是一种先进先出(FIFO, First In First Out)数据结构,常用于任务调度、广度优先搜索(BFS)等场景。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      const int MAX = 1000;
      int queue[MAX];
      int front = 0, rear = 0;

      void enqueue(int x) {
      queue[rear++] = x;
      }

      int dequeue() {
      return queue[front++];
      }
    • 实际应用
      • 任务调度:常用于处理操作系统中的任务调度、打印队列等。
      • 广度优先搜索 (BFS):通过队列实现层序遍历。

3. 树结构

二叉树 (Binary Tree)
  • 定义与特性
    二叉树是每个节点最多有两个子节点的树结构,子节点分别称为左子节点和右子节点。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    };

    TreeNode* root = new TreeNode{1, nullptr, nullptr}; // 创建根节点
    root->left = new TreeNode{2, nullptr, nullptr}; // 添加左子节点
    root->right = new TreeNode{3, nullptr, nullptr}; // 添加右子节点
  • 遍历方式
    • 前序遍历:根节点 -> 左子树 -> 右子树
    • 中序遍历:左子树 -> 根节点 -> 右子树
    • 后序遍历:左子树 -> 右子树 -> 根节点
    • 层序遍历:按层访问,常用队列实现
      1
      2
      3
      4
      5
      6
      7
      // 递归前序遍历
      void preorder(TreeNode* root) {
      if (!root) return;
      cout << root->val << " ";
      preorder(root->left);
      preorder(root->right);
      }
二叉搜索树 (Binary Search Tree, BST)
  • 定义与特性
    二叉搜索树是一种有序二叉树,对于每个节点,其左子树的所有节点值小于该节点值,右子树的所有节点值大于该节点值。
    1
    2
    3
    4
    5
    6
    TreeNode* insert(TreeNode* root, int val) {
    if (!root) return new TreeNode{val, nullptr, nullptr};
    if (val < root->val) root->left = insert(root->left, val);
    else root->right = insert(root->right, val);
    return root;
    }
  • 操作
    • 插入:新元素按照顺序插入到合适位置。
    • 查找:从根节点开始,根据大小关系查找节点。
    • 删除:删除节点时需要考虑其子节点的调整。
  • 平衡性问题与优化
    • AVL树:通过旋转保持树的平衡,确保插入、删除、查找操作时间复杂度为O(log n)。
    • 红黑树:类似AVL树,但平衡条件稍弱,插入和删除更高效。
堆 (Heap)
  • 定义与特性
    堆是一棵完全二叉树,分为最大堆和最小堆。最大堆中每个节点的值都大于或等于其子节点,最小堆则相反。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void heapify(int arr[], int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;

    if (left < n && arr[left] > arr[largest]) largest = left;
    if (right < n && arr[right] > arr[largest]) largest = right;
    if (largest != i) {
    swap(arr[i], arr[largest]);
    heapify(arr, n, largest);
    }
    }