软考-软件评测师:知识点整理(六)——数据结构与算法

二叉树

二叉树:每个结点最多两个孩子:左孩子、右孩子。

满二叉树:每一层结点全部长满,没有空缺。

完全二叉树:最后一层从左往右排,右边可以为空,左边不能缺。

斜树:全部只有在左/右孩子,最差的形态。

遍历

顺序是根据根的前后顺序定的,左右顺序永远不变。

  • 前序

根->左->右

  • 中序

左->根->右

  • 后序

左->右->根

确认唯一二叉树

  1. 前序+中序->唯一二叉树
  2. 后序+中序->唯一二叉树
  3. 前序+后续->不能确定唯一二叉树

计算公式

深度/层数:h(根是第一层)

结点总数:n

叶子结点树:n0

度1结点:n1

度2结点:n2

结点的度:该结点拥有的子节点个数。

度数公式

叶子结点数=度2节点树+1


结点总数

n= n0+ n1+ n2


二叉树层数结点范围

第k层最多结点数=2^(k-1)

深度为h的二叉树总结点最大值(满二叉树)=2^h-1

深度为h的二叉树总节点最小值=2^(h-1)


深度范围

n个结点的二叉树:

最小深度(完全二叉树)=(log2n)+1

最大深度(斜树)=n


完全二叉树专有公式

总结点为n,度1结点只有0或1个

编号规则(根=1,从上到下、从左到右):

  1. 结点i的左孩子=2i
  2. 结点i的右孩子=2i+1
  3. 结点i的双亲=i/2

线索二叉树

作用:利用空指针,保存前驱、后继,方便遍历。

N个结点二叉树,空链域数量=n+1


哈夫曼树(最优二叉树)

规则:每次选权值最小的两个结点合并,新的结点权值=两数之和。

特点

  1. 没有度为1的节点
  2. 叶子数n0,总结点=2n0-1
  3. 带权路径长度WPL最小

卡特兰数

卡特兰数:n个结点的二叉树的形态数。

公式:

常见卡特兰数:


邻接表

邻接表:给每一个顶点开一个链表(或动态数组),只存它直接相连的点和边权。

邻接矩阵:行列数都为节点个数,如节点数为5,边为7,则为5*5的矩阵。


链表

循环链表:在单向链表(或双向链表)基础上,令表尾节点的指针指向表中的第一个节点,构成循环链表。特点是可以从表中任何节点可是遍历整个链表。

排序算法

各种排序算法的时间复杂度和空间复杂度:

时间复杂度:衡量算法运行时间随着数据量n增长的变化趋势。平均情况,指的是随机数据下的表现。最坏情况,是指数据完全有序/逆序时的最差表现。O(1)=固定几次。O(n)=跟着数据量线性走。O(n2)=双层循环。O(nlogn)=分层+分治,每层n,层数logn。

空间复杂度:衡量算法额外需要的内存空间,重点看除了原数组外,额外开了多少空间。O(1)=原地排序,几乎不需要额外的空间。O(log2n)=递归栈的深度带来的额外空间。O(n)=需要额外开一个和原数组一样大的辅助数组。

  • 稳定:相等元素相对位置不变。
  • 不稳定:会乱序。
时间复杂度 排序算法 记忆
O(n^2) 冒泡排序 都是两层循环,一层遍历轮数,一层内部逐个对比,嵌套循环->n*n
O(n^2) 插入排序 都是两层循环,一层遍历轮数,一层内部逐个对比,嵌套循环->n*n
O(n^2) 选择排序 都是两层循环,一层遍历轮数,一层内部逐个对比,嵌套循环->n*n
O(nlogn) 快速排序 都是分治思想,拆分logn层,每层干n次活
O(nlogn) 归并排序 都是分治思想,拆分logn层,每层干n次活
O(nlogn) 堆排序 都是分治思想,拆分logn层,每层干n次活
复制代码
#include <iostream>
#include <vector>
using namespace std;

​
// 打印数组
void print(int arr[], int n)
{

   for(int i = 0; i < n; i++)
       cout << arr[i] << " ";
   cout << endl;
}
​
int main()
{

   int arr[] = {5,3,8,4,2};
   //自动算数组长度,仅限普通静态数组,不能用在函数内部
   int n = sizeof(arr) / sizeof(arr[0]);
   return 0;
}

插入排序类

直接插入排序

排序方式:把数组分为左边有序区+右边无序区,每次那一个无序区的第一个往前挨个比较,插到有序区的合适位置。

举例:[5,3,8,4,2],设[5]为有序,[3,8,4,2]为无序

  • 3,5,8,4,2\],此时\[8,4,2\]为无序

  • 3,4,5,8,2

  • 2,3,4,5,8

时间复杂度:最好(完全有序)时,每个数只比一次O(n);平均/最坏时,每个元素都要往前挪、对比多次O(n^2)。

空间复杂度:仅临时变量存待插入值O(1)。

稳定性:稳定。

适用场景数据基本有序,小规模数据,希尔排序的底层基础。

  • 优点:稳定、原地、简单;数据越有序,越快,最好O(n);不需要大量交换,只是后移。
  • 缺点:最坏/平均O(n^2);大数据非常慢。
复制代码
// 直接插入
void insertSort(int arr[], int n)
{
   for(int i = 1; i < n; i++)
  {
       int temp = arr[i];
       int j;
       for(j = i - 1; j >= 0 && arr[j] > temp; j--)
      {
           arr[j+1] = arr[j];
      }
       arr[j+1] = temp;
  }

}

Shell排序(希尔排序,缩小增量插入)

排序方式:先取一个增量gap(gap=数组长度/2,向下取整),把数组按gap分组(按下标每隔一个gap取一个),每组内部做插入排序,不断缩小gap,直到gap=1退化为普通插入。大的数快速跳到后面,减少后期挪动次数。

举例:[5,3,8,4,2],取gap=2->gap=1

  • gap=2,分组为[5,8,2]、[3,4],分别排序为[2,5,8]和[3,4],填回原来的下标,合并数组[2,3,5,4,8]
  • gap=1,退化为直接排序,得到[2,3,4,5,8]

时间复杂度:平均O(n^1.3),最坏O(n^2)。

空间复杂度:没有额外的数组O(1)。

稳定性:不稳定。

适用场景: 中等规模数据;嵌入式、简单工程轻量排序;不需要严格稳定的场景。

  • 优点: 插入排序优化版,远远快于冒泡、插入、选择;平均O(n^1.3)。中等数量够用;原地排序O(1)。
  • 缺点: 不稳定;增量规则不统一,数学逻辑复杂;最坏还是O(n^2)。
复制代码
// 希尔
void shellSort(int arr[], int n)
{
   int gap = n / 2;
   while(gap > 0)
  {
       for(int i = gap; i < n; i++)
      {
           int temp = arr[i];
           int j;
           for(j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
          {
               arr[j+gap] = arr[j];
          }
           arr[j+gap] = temp;
      }
       gap /= 2;
  }

}

选择排序类

直接选择排序(简单选择排序)

排序方式:每一轮在无序区找最小值,和无序区第一个位置交换,一轮锁定一个最左侧的有序数。

举例:[5,3,8,4,2]

  • 找全局最小2,和第一位交换->[2,3,8,4,5]
  • 在[3,8,4,5]里面找最小值3->[2,3,8,4,5]
  • 2,3,4,8,5

  • 2,3,4,5,8

时间复杂度:不管有序无序,每轮都要遍历找最小值,固定比较次数为n(n-1)/2,最好/最坏/平均时,O(n^2)。

空间复杂度:仅存最小值下标,O(1)。

稳定性:不稳定,远距离交换会打乱相等元素。

适用场景: 对交换次数有要求的场景;极小量数据,不在乎时间。

  • 优点: 思路简单,交换次数少;原地排序O(1)。
  • 缺点: 不稳定,不论是否有序,都要全遍历,固定O(n^2),效率差。
复制代码
// 简单选择
void selectSort(int arr[], int n)
{
   for(int i = 0; i < n; i++)
  {
       int minIdx = i;
       for(int j = i + 1; j < n; j++)
      {
           if(arr[j] < arr[minIdx])
               minIdx = j;
      }
       swap(arr[i], arr[minIdx]);
  }
}

堆排序

排序方式:把数组建立成大顶堆(父节点≥子节点),堆顶是最大值,交换到数组末尾(末尾有序),剩余元素重新调整为大顶堆,循环执行。

举例:[5,3,8,4,2]

  • 建大顶堆[8,4,5,3,2]
  • 堆顶最大值和最末尾值交换,保证最大值放末尾->[2,4,5,3,8],调整剩余堆
  • 堆顶5放在倒数第二->[2,4,3,5,8]
  • ......
  • 2,3,4,5,8

建堆规则:必须是完全二叉树,一层一层建满,不能空位置、不能跳格、不能随便挂节点。

下沉规则:只和左右两个孩子比,谁大和谁换。

顶尾交换规则:只和最大的孩子比,比如此时是第二轮,5的最大孩子是4,则和该分支最小的交换。

时间复杂度:建立堆O(n),每次调整堆O(logn)共n次,总的为O(nlogn)。

空间复杂度:原地在数组上建堆、调整,只用到临时变量,O(1)。

稳定性:不稳定。

适用场景: 内存受限、不能开额外数组;只需要找出最大/最小的前K个元素;操作系统、优先队列、任务调度。

  • 优点: 稳定O(nlogn);原地排序O(1),不占额外内存;适合动态取最佳(优先队列)。
  • 缺点:不稳定;缓存不友好,实际速度略慢于快排;建堆、下沉逻辑复杂。
复制代码
// 下沉调整
void adjustHeap(int arr[], int idx, int len)
{
   int temp = arr[idx];
   // 左孩子
   for(int i = idx * 2 + 1; i < len; i = i * 2 + 1)
  {
       // 找左右孩子最大
       if(i+1 < len && arr[i+1] > arr[i])
           i++;
       if(arr[i] > temp)
      {
           arr[idx] = arr[i];
           idx = i;
      }
       else break;
  }
   arr[idx] = temp;
}​

// 堆排序
void heapSort(int arr[], int n)
{
   // 初始建大顶堆:从最后一个非叶子结点往前
   for(int i = n/2 - 1; i >= 0; i--)
       adjustHeap(arr, i, n);
   

   // 逐轮交换堆顶+末尾,再调整
   for(int i = n - 1; i > 0; i--)
  {
       swap(arr[0], arr[i]);
       adjustHeap(arr, 0, i);
  }
}

交换排序类

冒泡排序

排序方式:相邻两个元素两两相比,前面比后面大就交换,一轮下来最大的数沉到最后。

举例:[5,3,8,4,2]

  • 3,5,8,4,2\]-\>\[3,5,4,8,2\]-\>\[3,5,4,2,8

  • 3,4,5,2,8\]-\>\[3,4,2,5,8

  • 3,2,4,5,8\]-\>\[2,3,4,5,8

时间复杂度:最好(本来就有序)时,加标记,只走一轮O(n);最坏(完全逆序)时,双层循环O(n^2);平均为O(n^2)。

空间复杂度:只用了临时变量交换、标记位,不开新数组、不递归,O(1)。

稳定性:稳定,相等值不交换。

适用场景:数据量很小、对效率无要求;基本有序的少量数据。

  • 优点:逻辑简单、代码短;稳定排序;原地排序,空间O(1);数据接近有序时效率比较高。
  • 缺点:时间复杂度高O(n^2);大量无效交换,效率低;不适合大量数据。
复制代码
// 冒泡
void bubbleSort(int arr[], int n)
{
   for(int i = 0; i < n - 1; i++)
  {
       for(int j = 0; j < n - 1 - i; j++)
      {
           if(arr[j] > arr[j+1])
               swap(arr[j], arr[j+1]);
      }
  }
}

快速排序

排序方式:分治+基准,选一个基准数,双指针(小的放左边,大的放右边),基准归位,左边右边递归重复。

基准归位:定义左右两个指针,L负责找比基准大的,R负责找比基准小的,两个指针分别往数组另一边走,找到比基准大/小的树,指针指着的两个数互换,直到左右指针碰面,一轮结束。左右指针碰面后,基准与当前数交换。基准选最左边,必须先动右指针R;基准选最右边,必须先动左指针L。
举例:[5,3,8,4,2],选5为基准。

  • 小于5的放左边,大于5的放右边,放完之后一趟就有[2,3,4,5,8]

时间复杂度:平均时,每次对半分,递归层数log2n,每一层一共处理n个数据,O(nlogn);最坏(完全有序/逆序)时,每次划分一边一个,一边n-1个,递归层数变成了n,O(n^2)。

空间复杂度:额外的空间用于递归调用栈。平均递归深度为log2n,O(logn);最坏深度n,O(n)。

稳定性:不稳定。

适用场景: 大数据量首选;日常开发、系统内置排序底层(C++/Java底层都是改良快排);不要求排序稳定的场景。

  • 优点: 平均O(nlogn),所有排序中最快;常数时间小、实际运行效率极高;递归实现简洁。
  • 缺点: 不稳定;最坏退化O(n^2);递归占用栈空间O(logn)。
复制代码
// 快速排序
void quickSort(int arr[], int left, int right)
{
   if(left >= right) 
    return;​

   int i = left, j = right;
   int pivot = arr[left];

   while(i < j)
  {
       // 右指针找 小于pivot
       while(i < j && arr[j] >= pivot)
           j--;

       // 左指针找 大于pivot
       while(i < j && arr[i] <= pivot) 
           i++;

​       if(i < j) 
           swap(arr[i], arr[j]);
  }

   // 指针相遇,交换基准
   arr[left] = arr[i];
   arr[i] = pivot;

   quickSort(arr, left, i - 1);
   quickSort(arr, i + 1, right);
}

归并排序

排序方式:分治+合并,不断把数组对半劈开,直到每组只有一个元素(天然有序),开一个辅助数组,把两个有序的小数组,按大小合并成大且有序的数组。

举例:[5,3,8,4,2]

  • 拆分:[5,3,8]、[4,2]
  • 继续拆:[5,3]、[8]、[4]、[2]
  • 继续拆:[5]、[3]、[8]、[4]、[2]
  • 合并:[3,5]、[2,4]
  • 3,5,8\]、\[2,4\],合并\[2,3,4,5,8

时间复杂度:拆分层数为log2n,每层合并总要处理n个元素,最好/最坏/平均都为O(nlogn)。

空间复杂度:合并时必须单独开一个辅助数组,和原数组大小一样,固定的额外空间为O(n)。

稳定性:稳定。

适用场景 : ++必须保证排序稳定++;海量数据、磁盘外排序;大数据且不缺内存的场景。

  • 优点: 时间绝对稳定O(nlogn);稳定排序,相等元素位置不变;适合外排序,数据太大放不了内存。
  • 缺点: 需要额外辅助数组,空间O(n);常数开销大,实际速度比快排慢。
复制代码
// 合并
void merge(int arr[], int temp[], int left, int mid, int right)
{
   int i = left, j = mid + 1, k = left;
   while(i <= mid && j <= right)
  {
       if(arr[i] <= arr[j])
           temp[k++] = arr[i++];
       else
           temp[k++] = arr[j++];
  }

   while(i <= mid)
       temp[k++] = arr[i++];

   while(j <= right)
       temp[k++] = arr[j++];

   for(int p = left; p <= right; p++)
       arr[p] = temp[p];
}

// 归并递归拆分
void mergeSort(int arr[], int temp[], int left, int right)
{
   if(left >= right) 
       return;

   int mid = (left + right) / 2;
   mergeSort(arr, temp, left, mid);
   mergeSort(arr, temp, mid+1, right);
   merge(arr, temp, left, mid, right);
}

​
// 统一调用入口
void mergeSort(int arr[], int n)
{
   int* temp = new int[n];
   mergeSort(arr, temp, 0, n-1);
   delete[] temp;
}

基数排序

排序方式:不是比大小,而是按每一位排序,比如个位、十位、百位......依次桶排序。

时间复杂度:d为位数,r为基数,O(d*(n+r))。

空间复杂度:需要桶容器O(n+r)。

适用场景: 大量整数、手机号、学号、身份证等;位数不多的批量数据。

  • 优点: 时间线性O(d*(r+n)),数据极大时碾压比较排序;稳定。
  • 缺点: 只能排整数、字符串这类固定位数数据;额外空间大;不能通用所有数据。

查找算法

查找分类

  1. 静态查找:数据不变(顺序、二分、分块)。
  2. 动态查找:数据频繁增删(哈希、二叉树排序)。
  3. 有序查找、无序查找

顺序查找(线性查找)

查找原理:不管数组有序无序,从第一个元素挨个往后对比,找到就返回下标,遍历完没找到就返回-1。

复杂度:最好O(1),第一个就是;最坏/平均O(n)。

适用场景:数据量很小,数据无序,没法二分。

  • 优点:简单、支持无序数组、链表也能用。
  • 缺点:数据量大极慢。

折半查找(二分查找)

查找原理:数组必须提前升序/降序有序,取中间mid,目标<mid,去左边找,否则去右边找。

时间复杂度:O(log2n)极快

空间复杂度:O(1)

适用场景:有序静态数组;频繁查找、很少修改。

  • 优点:效率高、大数据首选。
  • 缺点:必须有序;插入删除麻烦,要重排。
复制代码
// 二分查找 非递归
int binarySearch(int arr[], int n, int key)
{
   int left = 0;
   int right = n - 1;​

   while(left <= right)
  {
       int mid = (left + right) / 2;

       if(arr[mid] == key)
           return mid;

       else if(key < arr[mid])
           right = mid - 1;  // 左半区
       else
           left = mid + 1;   // 右半区
  }
   return -1;
}

或递归二分:

int binarySearchRec(int arr[], int l, int r, int key)
{

   if(l > r)
     return -1;

   int mid = (l + r) / 2;
   if(arr[mid] == key) 
      return mid;

   else if(key < arr[mid]) 
       return binarySearchRec(arr, l, mid-1, key);
   else 
       return binarySearchRec(arr, mid+1, r, key);
}

分块查找

查找原理:把数组分为若干块,建立索引表,记录每块最大值、块起始位置,先查索引确定目标在哪一块,再在块内顺序查找。

复杂度:平均O(√n),介于顺序O(n)和二分O(logn)中间。

适用场景:数据量大、局部有序、整体难排序;介于顺序和二分之间的折中场景。

  • 优点:不用全局有序;增删比二分方便。
  • 缺点:需要额外的索引空间;效率不如二分。

二叉排序树查找

二叉排序树的定义规则:左子树所有节点值<根节点值;右子树所有节点值>根节点值;左右子树同样满足上面要求。

查找原理:从根节点开始,目标值更小->走左子树,否则走右子树;相等则查找成功;走到空结点则查找失败。

时间复杂度:树比较平衡,理想情况O(logn);最坏(斜树/单链)时,数据有序插入,树退化成一条直线O(n);平均O(logn)。

空间复杂度:非递归查找O(1);递归查找O(logn)~O(n)。

适用场景

  • 动态数据场景,频繁查找、插入、删除;
  • 不需要严格平衡、追求实现简单;
  • 数据库、索引、容器底层基础结构;
  • 适合频繁修改的数据集,不适合固定静态有序数组。
  • 优点:支持动态数据;理想平衡状态下,查找效率接近二分查找O(logn);不需要整片连续有序数组,存储空间灵活;天然自带大小顺序,方便范围查询。
  • 缺点:数据有序插入时,容易变成斜树,效率退化到O(n);树结构复杂,实现、维护比数组麻烦;查询效率极度依赖树的平衡;不适合静态、固定不变的海量数据。
复制代码
// 二叉排序树查找,非递归

TreeNode* BST_Search(TreeNode* root, int key)
{
   TreeNode* p = root;
   while(p != nullptr)
  {
       if(key == p->val)
           return p;       // 找到

       else if(key < p->val)
           p = p->left;    // 小走左
       else
           p = p->right;   // 大走右
  }
   return nullptr; // 没找到
}

// 二叉排序树查找,递归
TreeNode* BST_Search_Rec(TreeNode* root, int key)
{
   if(root == nullptr)
       return nullptr;

​   if(key == root->val)
       return root;

   else if(key < root->val)
       return BST_Search_Rec(root->left, key);
   else
       return BST_Search_Rec(root->right, key);
}

哈希查找(散列查找)

查找原理:通过哈希函数,直接算出存放的下标,一步定位。

哈希冲突:两个不同的key,算出了同一个下标。比如哈希数组长度为5,数字7和12算出来下标都为2。

复杂度:理想无冲突O(1);最坏O(n)。

适用场景

  1. 高频增删改;
  2. 海量数据缓存、集合、map底层。
  • 优点:查找、插入、删除都极快。
  • 缺点:有哈希冲突;无序、不能范围查找。
相关推荐
想带你从多云到转晴2 小时前
06、数据结构与算法---二叉树
java·数据结构·算法
酉鬼女又兒2 小时前
Leetcode 26.删除有序数组中的重复项 双指针巧解有序数组去重:从快慢指针到原地修改算法的精髓
java·数据结构·算法·leetcode·职场和发展·蓝桥杯·排序算法
承渊政道2 小时前
【动态规划算法】(斐波那契数列模型详解)
数据结构·c++·学习·算法·leetcode·macos·动态规划
笨笨饿2 小时前
# 67_MCU的几大分区
数据结构·单片机·嵌入式硬件·算法·机器人·线性回归·个人开发
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 230. 二叉搜索树中第 K 小的元素 | C++ 栈迭代中序遍历
c++·算法·leetcode
大熊背2 小时前
ISP Pipeline中Lv实现方式探究之六--lv值计算再优化
网络·算法·自动曝光·lv
RTC老炮2 小时前
WebRTC下FlexFEC算法架构及原理
网络·算法·音视频·webrtc
xin_nai2 小时前
LeetCode热题100(Java)(2)双指针
算法·leetcode·职场和发展
七颗糖很甜2 小时前
预警!超级厄尔尼诺即将登场:2026-2027年全球气候或迎“极端狂暴模式”
java·大数据·python·算法·github