简单选择排序
描述: 简单选择排序,也称直接选择排序。排序过程如下:
- 找出第一个元素起到后面所有元素中的最小元素,和第一个元素不相等时相互交换位置;
- 找出第二个元素起到后面所有元素中的最小元素,和第二个元素不相等时相互交换位置;
- 以此类推,直到第n-1个元素。
java代码
public void selectSort(int[] arr) {
int min, temp;
for (int i = 0; i < arr.length - 1; i++) { // n个元素需要n-1趟查找交换
min = i; // 假设i为最小元素下标
for (int j = i + 1; j < arr.length; j++) { // 循环遍历后面元素,遇到更小的元素就将下标赋给min
if (arr[j] < arr[min]) {
min = j;
}
}
if (i != min) { // 假定的最小元素和实际的最小元素不是同一个就交换位置
temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
}
算法分析
- 时间复杂度 O ( n 2 ) Ο(n^2) O(n2),最好情况n个初始元素正序,只比较不移动;最坏情况逆序,n个元素交换n-1次,每次交换移动3次共移动3(n-1)次。最好最坏情况都要比较 ∑ i = 1 n − 1 i ≈ n 2 2 \sum_{i=1}^{n-1}i \approx \frac{n^2}{2} ∑i=1n−1i≈2n2,所以时间复杂度 O ( n 2 ) Ο(n^2) O(n2)。
- 空间复杂度 O ( 1 ) Ο(1) O(1),每次元素交换时只需要一个辅助空间temp。
- 稳定性不确定,就上面代码来说属于不稳定排序,比如[2,2,1]第一次交换之后就将第一个2交换到第二个2后面了,这是因为元素跳越交换的原因。当改变这种移动方式采用相邻元素后移就会使其变成一种稳定排序,此时也就是简单选择和直接插入的综合了。
- 可用于链式结构。
- 总的移动次数较少,与元素个数n成线性关系,当单个元素占用空间较多时,显然移动的开销大于比较的开销,此时该方法要比直接插入排序好。
堆排序
描述: 将待排序数组看成是完全二叉树的顺序存储结构,利用父结点和子结点之间对应的映射关系也就是对应的数组下标之间的关系,在当前无序序列中选出最大或最小元素,交换到数组最后并对逻辑上去掉这个元素后形成的新数组重复这个操作。
堆排序利用了堆顶元素最大(对应大根堆)或最小(对应小根堆)的特征,本文以大根堆为例。
个人觉得堆排序在几种排序算法中算是比较难的,难点在于对堆调整过程的理解。
上面废话了~,下面这段话重点理解。
堆调整的过程简要概括就是将一个父结点的左右子结点中的最大者与父结点比较,大于父结点就交换位置,然后以发生交换的子结点为父结点按照上面继续递归交换下去,直到叶子结点。
从上面堆调整的过程可以看出以某个结点为根结点进行堆调整其实就是对该结点及下层所有结点进行一次筛选的过程,使值大的结点上移值小的结点下移,总能保证上层结点的值大于等于下层结点的值。整个过程过程有点类似冒泡排序。
理解了堆调整建初堆就不难看明白了,从最后一个非叶子结点开始调整,依次调整到根结点,这样就保证了树的每一层结点,都大于等于下一层的所有结点,也就实现了堆顶元素最大,建成了大根堆。
剩下的就是不断的交换,调整,交换…,直到序列完全有序。
相关概念
- 堆:首先是一棵完全二叉树,且所有的父结点大于等于子结点或所有的父结点小于等于子结点
- 大根堆:所有父结点大于等于子结点的堆
- 小根堆:所有父结点小于等于子结点的堆
java代码
import java.util.Arrays;
public class HeapSort {
// 外部调用接口
public static void heapSort(int[] arr) {
creatHeap(arr);
for (int i = arr.length - 1; i > 0; i--) {
// 元素交换,作用是把堆顶的根元素交换到数组最后
swap(arr, 0, i);
// 去掉最后一个叶子结点后重新调整为新堆
adjustHeap(arr, 0, i);
}
}
// 建初堆,目的是将无序数组调整为堆,实现方式是从最后一个非叶子结点起到最后一个根节点依次进行堆调整
private static void creatHeap(int[] arr) {
// 根结点即数组首元素从0开始,所以最后一个非叶子结点p=arr.length/2-1
for (int p = arr.length / 2 - 1; p >= 0; p--) {
adjustHeap(arr, p, arr.length);
}
}
// 调整堆
private static void adjustHeap(int[] arr, int p, int length) {
// c、p分别表示左子结点、父结点索引,length表示要调整数组长度
for (int c = 2 * p + 1; c < length; c = 2 * c + 1) {
// 右子结点存在时找出左右子结点中最大结点的索引
if (c + 1 < length && arr[c] < arr[c + 1]) {
c++;
}
// 子结点不大于父结点不用交换位置直接退出循环
if (arr[p] >= arr[c]) {
break;
}
// 否则交换父结点子结点的位置,因为子结点发生变化导致以子结点为根的子树收到影响
// 所以要对该子结点的子树继续进行调整
swap(arr, c, p);
p = c;
}
}
// 交换函数
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 测试
public static void main(String[] args) {
int[] arr = new int[1000];
for (int i = 0; i < 1000; i++) {
// 随机生成1000个[0,999]整数
arr[i] = (int) (Math.random() * 1000);
}
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
}
算法分析
- 时间复杂度
O
(
n
log
2
n
)
Ο(n\log_2{n})
O(nlog2n),从两方面分析:一是建初堆的时间复杂度,二是调整堆的时间复杂度。两方面都按最坏情况下计算:
建初堆:设初始序列对应的完全二叉树深度为k,每个非终端结点都要自上而下进行递归筛选,由于第 i 层的结点数小于等于 2 i − 1 2^{i-1} 2i−1,且第 i 层结点最大下移深度h-i,每下移一层要做两次比较,所以总的比较次数 ∑ i = h − 1 1 2 i − 1 ∗ 2 ( h − i ) ≤ 4 n \sum_{i=h-1}^{1}{2^{i-1}*2(h-i)} \leq 4n ∑i=h−112i−1∗2(h−i)≤4n(怎么算的有数学好的大神来教教我~)
调整堆:n个元素共要调整堆n-1次,每次调整将堆顶元素下移的时间复杂度为 2 l o g 2 n 2log_2{n} 2log2n,n为每次调整堆的元素个数,所以总的时间复杂度为 2 ∑ n − 1 2 l o g 2 n < 2 l o g 2 n ! 2\sum_{n-1}^{2}{log_2{n}} < 2log_2{n!} 2∑n−12log2n<2log2n!,因为 n ! ≤ n n n! \leq n^n n!≤nn,所以 l o g 2 n ! ≤ n l o g 2 n log_2{n!} \leq nlog_2{n} log2n!≤nlog2n。
所以最坏情况下总的时间复杂度 O ( n log 2 n ) Ο(n\log_2{n}) O(nlog2n),研究表明平均性能接近于最坏性能。研究略~ - 空间复杂度 O ( 1 ) Ο(1) O(1),仅在元素交换时需要一个辅助空间temp。
- 排序不稳定,从调整堆部分第29行的if语句可以看出当左右子结点相等时会将左子结点优先调到根结点位置从而先被交换到数组后面,排序完成后左右子结点相对位置发生改变。
- 只能用于顺序存储结构,由完全二叉树中父子结点之间的内在关系决定。
- 从时间复杂度的分析可以看到建初堆时比较次数较多,所以不适合初始元素较少的情况。与快速排序相比好于快排的最坏情况 O ( n 2 ) Ο(n^2) O(n2),但一般情况下初始元素较多时快排仍是所有内部排序中最快的。