程序设计 = 数据结构 + 算法。
算法是解决特定问题的有限步骤。
一、算法基础概念
1. 算法设计五大要求
- 正确性:合法输入得到合理结果;非法输入做对应处理;刁难测试用例也可正常运行。
- 可读性:代码便于阅读交流,高内聚、低耦合。
- 健壮性:面对非法输入,不会直接崩溃,能够做异常处理。
- 高效率(时间复杂度):执行操作次数少。
- 低存储(空间复杂度):算法运行额外占用内存少。
2. 时间复杂度、空间复杂度
- 时间复杂度 :度量算法执行时间随数据规模
n增长的变化趋势,使用大 O 表示法,不统计绝对运行时间。 - 空间复杂度 :算法执行过程中额外开辟内存空间 随数据规模
n的变化趋势,不统计原始输入数据占用空间。
大 O 阶推导三条规则:
- 用常数 1 代替所有加法常数;
- 修改后函数中只保留最高阶项;
- 如果最高阶项存在且系数不为 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)稳定性意义:业务上如果需要保留原始相等数据的先后关系,必须选择稳定排序算法。