选择排序的基本思想是:每一趟(如第i趟)在后面n-i+1个待排序元素中选择关键字最小的元素,作为有序子序列的第i个元素,执行n-1趟,最后那个不用选了。
主要包括:简单选择排序和堆排序。
简单选择排序
void selectSort(int arr[], int n)
{
int i, j, min;
for (i = 0; i < n; ++i){
min = i;//最小元素下标
for (j = i + 1; j < n; ++j)
if (arr[j] < arr[min])
min = j;
if (min != j)
swap(arr[i],arr[min]);
}
}
空间复杂度: O(1)
时间复杂度:为了找到最小的,必须要和无序区的所有元素比较,比较的复杂度固定为 O(n2) ;移动的复杂度看情况:
- 最好情况下,最小值就在首位,不用交换,那么移动的复杂度为 0 ;
- 最差情况下,每轮最小值都在后面,则要进行一次交换,一次交换为
3 , n−1 次一共为 3(n−1) ,复杂度为 O(n) ;
加上比较的复杂度,则时间复杂度固定为 O(n2) 。
稳定性:不稳定,如 3 3* 1,1 3互换,3和3*位置反了。
堆排序
堆常用于实现优先队列。
先看一下堆的定义:
n个关键字序列L[1..n]称为堆,当且仅当该序列满足以下一个:
- L(i)≤L(2i) 且 L(i)≤L(2i+1) , 1≤i≤⌊n/2⌋
- L(i)≥L(2i) 且 L(i)≥L(2i+1) , 1≤i≤⌊n/2⌋
满足1为小顶堆,满足2为大顶堆。
如图:
创建大顶堆算法思路:
1. 最后一个节点n的下标为n-1,它的父节点下标为
⌊(n−2)/2⌋
,然后从这个父节点下标开始,执行以下操作:
2. 比较父节点的左右两个孩子,大的那个如果比父节点还大,则交换;
3. 如果有交换,以进行交换的那个子节点为子树向下进行调整,递归2-3.
4. 执行完2和3后,父节点下标减一,直至到达根节点。
void buildMaxHeap(int arr[], int len)
{
int i;
for ( i = len / 2; i > 0; --i)
adjustDown(arr, i, len);
}
void adjustDown(int arr[], int k, int len)
{
arr[0] = arr[k];//arr[0]暂时存储子树的根节点
int i;
for (i = 2 * k; i <= len; i *= 2){//沿key较大的子节点向下筛选
if (i < len&&arr[i] < arr[i + 1])
++i;
if (arr[0] >= arr[i])
break;//父节点大于左右两个孩子,子树没问题了,直接退出
else{
arr[k] = arr[i];//大的那个孩子调到父节点
k = i;//k来记录空缺的地方
}
}
arr[k] = arr[0];//找到树下合适的地方落脚
}
建堆的时间复杂度:在n各元素上建堆的时间复杂度为 O(n)
假如有N个节点,那么高度为H=logN,最后一层每个父节点最多只需要下调1次,倒数第二层最多只需要下调2次,顶点最多需要下调H次,而最后一层父节点共有 2(H−1) 个,倒数第二层公有 2(H−2) ,顶点只有1(2^0)个,所以总共的时间复杂度为 s=1∗2(H−1)+2∗2(H−2)+...+(H−1)∗21+H∗20 ,乘2相减中间有个等比数列,将H代入后s= 2N−2−log2N ,近似的时间复杂度就是 O(N)
建完大顶堆后,顶元素就是最大值了,然后最后一个元素换到顶上,来一遍向下调整,如此n-1次即可。
void heapSort(int arr[], int len)
{
buildMaxHeap(arr, len);
int i;
for (i = len; i > 1; --i){
swap(arr[i], arr[1]);//最大值交换到最后
adjustDown(arr, 1, i - 1);//整理剩余的i-1个元素
}
}
堆的删除
只能删除堆顶的元素,实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次向下调整,代码同上。
时间复杂度: log2n
堆的插入
每次插入都是将先将新数据放在数组最后,然后执行一次向上调整。
void adjustUp(int arr[], int k)//k是堆元素的个数(首位不算),也是待向上调整的节点,
{
arr[0] = arr[k];
int i = k/2;
while (i > 0 && arr[0] > arr[i]){
arr[k] = arr[i];
k = i;//k来记录“空”位子
i = k / 2;
}
arr[k] = arr[0];
}
向上调整(插入)的时间复杂度:与数字大小有关,最多 log2n ,即向上到了根节点
整个堆排序的性能分析
空间复杂度: O(1)
时间复杂度:建堆为 O(n) 毋庸置疑,之后有n-1次向下调整的操作,每次调整的时间复杂度与树高有关,为 O(h) 。第i次向下调整,深度为 log2(n−i+1) ,每次向下调整都要比较深度次数次,网上很多都说每次比较都是 log2n 次,但我感觉排序的话n每次会减少1啊,最终值是 log2n!−1 ,在 O(n) 与 O(nlog2n) 之间。
稳定性:不稳定,如 1 2* 2,第一次变成 2* 1 2,即2*最大,与2位子反了
各算法复杂度表
加上前几篇的内容,表扩充为:
算法 | 平均时间复杂度 | 最好时间复杂度 | 最差时间复杂度 | 空间复杂度 | 稳定性 | 备注 |
---|---|---|---|---|---|---|
直接插入 | O(n2) | O(n) | O(n2) | O(1) | 稳定 | |
折半插入 | O(n2) | O(nlog2n) | O(n2) | O(1) | 稳定 | |
shell | O(n1.3) | O(n) | O(n2) | O(1) | 不稳定 | 和增量序列有关 |
冒泡 | O(n2) | O(n) | O(n2) | O(1) | 稳定 | 子序列全局有序,不同于插入排序 |
快排 | O(nlog2n) | O(nlog2n) | O(n2) | O(log2n) | 不稳定 | |
简单选择 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 | |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |