排序操作在计算机程序设计中分为外部排序和内部排序。我们一般所说的排序算法指的就是内部排序,即数据记录在计算机内存中进行排序。
上图为内部排序的脑图。下来九种排序算法我们一一介绍。
1、冒泡排序
(1)基本思想:
- 依次比较相邻两个数,小的放前边,大的放后边,依次比较,直到比较到最后两个数。得到最后位置的数为最大数。
- 所有数重复上边的操作,除了最后一个。以此类推,比较完所有的数。
假设 n 个数进行排序,则需要比较 n - 1 轮,第一轮比较 n - 1 次,之后每轮比较减少 1 次。
(2)代码实现:
第一个版本:
public static void bubbleSort(int[] a) {
int temp;// 临时变量
for (int i = 0; i < a.length - 1; i++) {// 需要比较 n - 1 轮
for (int j = 0; j < a.length - i - 1; j++) {// 每轮需要比较的次数减 1,第一轮比较 n - 1 次
if (a[j] > a[j + 1]) {
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
第一版本存在这样的问题:假设一数组长度为 6 ,故需要比较 5 轮,但会发现在第 4 轮就已完成,第 5 轮比较就没有意义了,所以还能继续优化。设置一个Boolean 变量 flag ,判断某轮比较中是否发生数据交换。若没有发生则表示排序完成,就可以立即停止。
第二个版本:
public static void bubbleSort(int[] a) {
boolean flag;//表示是否交换
int temp;// 临时变量
for (int i = 0; i < a.length - 1; i++) {// 需要比较 n - 1 轮
flag = false;
for (int j = 0; j < a.length - i - 1; j++) {// 每轮需要比较的次数减 1,第一轮比较 n - 1 次
if (a[j] > a[j + 1]) {
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
flag = true;
}
}
if(!flag) break;//如果为false则排序完成,跳出外层循环
}
}
(3)算法分析:
最坏时间复杂度:O(n^2) 最好时间复杂度:O(n) 故平均复杂度为:O(n^2)
稳定性:算法是比较相邻两个元数,交换也发生在这两个元数上,两个相邻元数值相等不会交换,排序前排序后相等的两个元数的相对顺序不会发生改变,故算法具有稳定性。
2、快速排序
(1)基本思想:(利用了递归与分治策略的思想)
- 从数组中取出一个数作为“基准”。
- 划分:所有比基准小的数放在其左边,所有比基准大的数放其右边,与其相等可放任意一边。
- 递归求解:将以上所划分的子序列通过递归继续执行快排算法,直到子序列不可分割为止。
一轮快排具体操作是:i 和 j 开始分别置于数组的最左端和最右端,设某个数为基准(一般取第 0 个数)。首先 j 先向左移动找到第一个小于“基准”的数并和基准相互交换,然后 i 向右移动找到第一个大于“基准”的数并和基准交换;重复这两步直到 i == j 。
其实实际排序中没有必要找到符合要求的数就和“基准”进行交换,因为只需要在 i == j 的位置才是最后基准的位置,所以现在现将 基准暂存在 a [0] 位上,j 和 i 各自移动,找到符合条件(比基准小 / 大)的数,直到一轮排序结束再将基准移动到正确位置。
(2)代码实现:
public static void quickSort(int [] a,int left,int right){
if(left > right) return;//若不满足直接跳出该方法
int i = left;//数组最左下标赋给 i
int j = right;//最右下标赋给 j
int pivotkey = a [left];//将最左端数值赋给 pivotkey 作为基准
while(i < j){
while(i < j && a [j] >= pivotkey)//j 从右向左找第一个小于基准的值
j--;
if(i < j){
a [i] = a [j];
i++;
}
while(i < j && a [i] < pivotkey)// i 从左向右找第一个大于基准的值
i++;
if(i < j){
a [j] = a [i];
j--;
}
}
a [i] = pivotkey;// i== j 时将基准放置 i 处
quickSort(a,left,i - 1);//递归调用
quickSort(a,i + 1,right);
}
(3)算法分析:
最坏时间复杂度:O(n^2) 最好时间复杂度:O(n logn) 故平均复杂度为:O(n logn)
稳定性:数组 { 5, 2, 6, 3, 2, 7 } 基准元素为 5 ,和第四个元素 2 交换,两个 2 的相对顺序发生改变,故算法不稳定。
3、简单选择排序
(1)基本思想:
- 设长度为 n 的无序数组,需要排序 n - 1 轮,每轮需要比较 n - i 次,从 n - i + 1 个元素中找到最小的数值和第 i 个元素交换。
- 第一轮需要比较 n - 1 次,从 n 个元素中找到最小数值和第一个元素交换。......
如图示例:数组长度为 8,需要比较 7 轮,第一轮需要比较 7 次,之后每轮比较次数减 1。
(2)代码实现:
public static void selectSort(int [] a) {
for (int i = 0; i < a.length-1; i++) {//第 i轮排序
int minIndex = i;//记录最小数的位置
for (int j = i+1; j < a.length; j++) {
if (a[j]<a[minIndex]) {
minIndex = j;//目前最小数的位置
}
}
if (minIndex != i) {//最小数和第 i 个元素交换
int temp = a[i];
a[i] = a[minIndex];
a[minIndex] = temp;
}
}
}
(3)算法分析:
该算法无论数组的初始排列如何,元素所需比较的次数相同,均为 n(n - 1)/ 2,所以时间复杂度最坏/最好/平均都是 O(n ^ 2)。
稳定性:数组{ 4,8, 3,4, 6} 一次选择的最小元素的值为 3 ,然后和第一个 4 交换,两个 4 的相对位置发生变化,故该算法不稳定。
4、堆排序
堆的定义:n 个元素的序列 {K1,K2,……,Kn} 当且仅当满足以下关系:
大顶堆完全二叉树: 小顶堆完全二叉树:
Ki <= K2i Ki >= K2i
Ki <= K2i+1 Ki >= K2i+1 (i = 1,2,3,……,n/2)
示例:
(1)基本思想:
- 先将一个无序序列构建成一个堆。
- 输出堆顶的最大值(最小值),将剩余 n - 1 个元素的序列重新构建一个堆,得到 n 个元素中的次大值(次小值)
- 反复执行上述第二步,最终能得到一个有序序列。
(2)代码实现:
// 堆排序
public static void heapSort(int[] a) {
// 1.构建大顶堆
for (int i = a.length / 2 - 1; i >= 0; i--) {
// 从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr, i, a.length);
}
// 2.调整堆结构+交换堆顶元素与末尾元素
for (int j = a.length - 1; j > 0; j--) {
// 将堆顶元素与末尾元素进行交换
int temp = a[0];
a[0] = a[j];
a[j] = temp;
adjustHeap(a, 0, j);// 重新对堆进行调整
}
}
public static void adjustHeap(int[] a, int i, int length) {
int temp = a[i];// 先取出当前元素i
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {// 从i结点的左子结点开始,也就是2i+1处开始
if (k + 1 < length && a[k] < a[k + 1]) {// 如果左子结点小于右子结点,k指向右子结点
k++;
}
if (a[k] > temp) {// 如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
a[i] = a[k];
i = k;
} else {
break;
}
}
a[i] = temp;// 将temp值放到最终的位置
}
(3)算法分析:
初始化堆为 O(n) ,交换堆元素并重建堆为 O(n logn) ,其时间复杂度最坏/最好/平均都是 O(n logn)
空间复杂度为 O( 1 )
稳定性:数组{ 8, 4, 5, 4 } 堆顶元素为 8,堆排序下一步把 8 和第二个 4 交换,最终排序完成后两个 4的相对顺序发生变化,故算法不稳定。
参考文章:https://blog.csdn.net/lutianfeiml/article/details/51958962