数据结构 --- 算法

程序设计 = 数据结构 + 算法。

算法是解决特定问题的有限步骤

一、算法基础概念

1. 算法设计五大要求

  • 正确性:合法输入得到合理结果;非法输入做对应处理;刁难测试用例也可正常运行。
  • 可读性:代码便于阅读交流,高内聚、低耦合。
  • 健壮性:面对非法输入,不会直接崩溃,能够做异常处理。
  • 高效率(时间复杂度):执行操作次数少。
  • 低存储(空间复杂度):算法运行额外占用内存少。

2. 时间复杂度、空间复杂度

  • 时间复杂度 :度量算法执行时间随数据规模n增长的变化趋势,使用大 O 表示法,不统计绝对运行时间。
  • 空间复杂度 :算法执行过程中额外开辟内存空间 随数据规模n的变化趋势,不统计原始输入数据占用空间。

大 O 阶推导三条规则:

  1. 用常数 1 代替所有加法常数;
  2. 修改后函数中只保留最高阶项
  3. 如果最高阶项存在且系数不为 1,去除相乘常数。

常见复杂度效率从小到大排序

O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)

  • O(1)常数阶:普通单步语句;
  • O(logn)对数阶:循环每次i *=2,二分查找;
  • O(n)线性阶:单层循环;
  • O(nlogn)线性对数阶:一层 n 循环,一层 logn 循环嵌套;
  • O(n^2)平方阶:两层 n 嵌套循环。

图像:复杂度曲线图,n 增大时,指数阶、阶乘阶增长爆炸;\(O(1)\)几乎水平不变。

二、查找算法

1. 二分查找(折半查找)

前提条件:序列必须有序

  • 思想:不断取中间元素比较目标值,每次排除一半查找区间。

  • 时间复杂度:O(logn)

  • 空间复杂度:O(1)(迭代版)

  • 缺点:只适用于有序数组;链表不适合二分查找,链表无法直接访问中间下标

    //二分查找 迭代版本
    int binary_search(int *a, int len, int key)
    {
    int left = 0;
    int right = len - 1;
    int mid = 0;
    while(left <= right)
    {
    mid = (left + right) / 2;
    if(a[mid] == key)
    {
    return mid; //找到,返回下标
    }
    else if(key < a[mid])
    {
    right = mid - 1; //目标在左半区间
    }
    else
    {
    left = mid + 1; //目标在右半区间
    }
    }
    return -1; //查找失败
    }

补充:顺序查找,不需要有序,时间复杂度O(n),逐个遍历对比。

三、排序算法

稳定性:两个相等的元素,排序完成后相对先后顺序不变,则算法稳定;否则不稳定。

1. 插入排序

核心思想:把待排序元素,逐个插入到前面已经有序的子序列合适位置,保证插入后序列依旧有序。

  • 时间复杂度:平均、最坏 O(n^2);最好(数据已经有序)O(n)

  • 空间复杂度:O(1),原地排序,不需要额外数组

  • 稳定性:稳定

  • 适合场景:小规模数据、数据基本接近有序的场景。

    //笔记源码片段
    for(i=1;i<len;++i)
    {
    tmp =a[i];
    j=i;
    while (j>0 && tmp < a[j-1])
    {
    a[j]=a[j-1];
    j--;
    }
    a[j]=tmp;
    }

2. 希尔排序

插入排序的改进版本

思想:设置增量inc,把数组按照增量分组,组内做直接插入排序;不断缩小增量,直到增量等于 1,做一次完整插入排序,完成整体排序。

(1)初始增量一般取len/2;每轮inc /=2;直到inc>0结束。

(2)先让数组整体基本有序,最后增量为 1 完成排序。

  • 时间复杂度:O(nlogn) ~ O(n^2)

  • 空间复杂度:O(1)

  • 稳定性:不稳定,分组交换会破坏相等元素相对位置。

    void shell_sort(int *a, int len)
    {
    int inc = 0;
    int i = 0, j= 0, tmp = 0;
    for (inc = len/2; inc > 0; inc /= 2)
    {
    for (i = inc; i < len; ++i)
    {
    tmp = a[i];
    j = i;
    while (j >= inc && tmp < a[j-inc])
    {
    a[j] = a[j-inc];
    j -= inc;
    }
    a[j] = tmp;
    }
    }
    }

3. 快速排序

分治思想,冒泡排序的改良。

思想:选取一个基准 key;一趟划分,把比基准小的放左边,比基准大的放右边;基准归位;递归对基准左边子序列、右边子序列重复划分,直到区间长度为 1。

  • 平均时间复杂度:O(nlogn);最坏(数据完全有序)退化 O(n^2)

  • 空间复杂度:O(logn),来自递归调用栈

  • 稳定性:不稳定

    void quick_sort(int *a, int begin, int end)
    {
    if (begin >= end)
    {
    return ;
    }
    int i = begin;
    int j = end;
    int key = a[i];
    while (i < j)
    {
    while (i < j && key <= a[j])
    {
    --j;
    }
    a[i] = a[j];
    while (i < j && key >= a[i])
    {
    ++i;
    }
    a[j] = a[i];
    }
    a[i] = key;
    quick_sort(a, i+1, end);
    quick_sort(a, begin, i-1);
    }

4. 冒泡排序

思想:相邻两个元素两两比较,如果逆序就交换;每一轮会把当前未排序区间最大元素 "冒泡" 到末尾。

  • 平均 / 最坏时间复杂度:O(n^2)

  • 空间复杂度:O(1)

  • 稳定性:稳定

    //冒泡排序
    void bubble_sort(int *a, int len)
    {
    int i = 0, j = 0;
    int tmp = 0;
    //外层控制轮数
    for(i = 0; i < len - 1; ++i)
    {
    //内层相邻比较,每轮结束末尾i个元素已经排好
    for(j = 0; j < len - 1 - i; ++j)
    {
    if(a[j] > a[j+1])
    {
    tmp = a[j];
    a[j] = a[j+1];
    a[j+1] = tmp;
    }
    }
    }
    }

5. 选择排序

思想:每一轮遍历未排序区间,找到最小元素的下标;把最小元素和未排序区间的第一个位置交换。

  • 平均 / 最坏时间复杂度:O(n^2)

  • 空间复杂度:O(1)

  • 稳定性:不稳定

    //选择排序
    void select_sort(int *a, int len)
    {
    int i = 0, j = 0;
    int min_idx = 0;
    int tmp = 0;
    for(i = 0; i < len - 1; ++i)
    {
    min_idx = i; //假定当前i位置是最小值下标
    //寻找未排序区间最小元素下标
    for(j = i + 1; j < len; ++j)
    {
    if(a[j] < a[min_idx])
    {
    min_idx = j;
    }
    }
    //把最小元素交换到i位置
    if(min_idx != i)
    {
    tmp = a[i];
    a[i] = a[min_idx];
    a[min_idx] = tmp;
    }
    }
    }

四、排序算法汇总对比表

排序算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 核心特点
插入排序 O(n2) O(n2) O(1) 稳定 适合小规模、接近有序数据
希尔排序 O(nlogn)∼O(n2) O(n2) O(1) 不稳定 分组插入,改进插入排序
冒泡排序 O(n2) O(n2) O(1) 稳定 相邻交换,效率偏低
选择排序 O(n2) O(n2) O(1) 不稳定 选择最小元素放置
快速排序 O(nlogn) O(n2) O(logn) 不稳定 分治思想,工业最常用

五、补充

(1)快速排序是实际工程中最常用排序;当数据量很小,插入排序效率表现很好。

(2)二分查找必须有序,无序数组不能直接二分。

(3)复杂度分析看最坏情况,用来评估算法性能上限。

(4)稳定性意义:业务上如果需要保留原始相等数据的先后关系,必须选择稳定排序算法。

相关推荐
个 人 练 习 生1 小时前
数据结构链表:带头双向循环链表
c语言·数据结构·经验分享·学习·其他·链表
血小板要健康2 小时前
二叉树 dfs 题型总结
算法·深度优先
我变成萤火虫8 小时前
河南萌新联赛2026第(四)场:南阳理工学院
数据结构·c++·算法·贪心算法·stl·动态规划
To_OC10 小时前
LC 239 滑动窗口最大值:从暴力超时到单调队列一遍过
javascript·算法·leetcode
.道阻且长.11 小时前
9.LeetCode算法习题讲解--滑动窗口--无重复字符的最长字串
算法·leetcode·职场和发展·哈希算法
ltl12 小时前
HNSW:图索引如何击败树索引
算法
曹牧14 小时前
C#:数字的定义和表示方式
算法·c#
Nil20814 小时前
leetcode 138随机链表的复制
算法·leetcode·链表
疯狂打码的少年16 小时前
【数据结构】图的遍历:深度优先搜索(DFS)
数据结构·笔记·算法·深度优先