五种排序方法
- 插入排序
- 直接插入排序
- 希尔排序
- 交换排序
- 冒泡排序
- 快速排序
- 选择排序
- 简单选择排序
- 堆排序
- 归并排序
- 分类排序
排序方法基本思想和算法描述
直接插入排序/冒泡排序/简单选择排序,这些简单算法所需时间复杂度大为O(n^2).
希尔排序/快速排序/堆排序/归并排序,这些较复杂算法的时间复杂度,平均情况下位O(nlog2n),有些最快情况下退化成O(n^2).
1 插入排序
插入排序的基本思想:
在一个已排序好的记录子集的基础上,每一步将下一个待排序的记录有序插入到已排序好的记录子集中,直到将待排序记录全部插入为止.
1.1 直接插入排序
算法思想:
直接插入排序是一种最基本的插入排序方法,其基本操作是将第i个记录插入到前面i-1个已排序好的记录中.
具体过程为:
将第i个记录的关键字Ki,顺次与其前面记录的关键字K(i-1),K(i-2),…,K0进行比较,将所有关键字大于Ki的记录一次向后移动一个位置,直到遇见一个关键字小于或者等于Ki的记录Kj,此时Kj后面必为空位置,将第i个记录插入到空位置即可.
完整的插入排序是从i=1开始的,也就是说,将第一个记录视为已排序好的单元素子集合,然后将第二个记录插入到单元素子集合中.i从1循环到length-1,即可实现完整的直接插入排序.
直接插入排序算法简便,比较适用于待排序记录数目较少且基本有序的情况.
public class InsertSort {
public static void insertSort(int a[]) {
for (int i = 1; i < a.length; i++) {
int key = a[i];
int j = i - 1;
while (j >= 0 && a[j] > key) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
}
public static void main(String[] args) {
int a[] = {3, 1, 5, 9, 11, 6, 99, 22, 55};
insertSort(a);
System.out.println(Arrays.toString(a));
}
}
1.2 希尔排序
算法改进要点:
直接插入排序,在待排序的关键字序列基本有序且关键字个数n较少时,其算法性能最佳.
希尔排序又称缩小增量排序,是一种基于插入思想的排序方法,它利用了直接插入排序的最佳性质,将待排序的关键字序列分成若干个较小的子序列,对子序列进行直接插入排序,是整个待排序序列排序好.
算法思想:
先将待排序记录分列分割成若干个”较稀疏”的子序列,分别进行直接插入排序.经过上述粗略调整,整个序列中的记录已基本有序,最后再对全部记录进行一次直接插入排序.
1. 首先选定记录间距离为di(i=1),在整个待排序记录序列中将所有间隔位d1的记录分成一组,进行组内直接插入排序.
2. 然后取i=i+1,记录间的距离为di(di
public static void shellInsert(int a[]) {
for (int gap = a.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < a.length; i++) {
int key = a[i];
int j = i - gap;
while (j >= 0 && a[j] > key) {
a[j + gap] = a[j];
j -= gap;
}
a[j + gap] = key;
}
}
}
public static void main(String[] args) {
int a[] = {3, 1, 5, 9, 11, 6, 99, 22, 55};
shellInsert(a);
System.out.println(Arrays.toString(a));
}
1.3 小结
插入类排序算法:
排序算法 | 改进思路 | 时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|---|---|
直接插入排序 | 基本排序方法 | O(n^2) | O(n) | O(n^2) | O(1) |
希尔排序 | 利用直接插入排序的最好情况:n比较小,基本有序 | O(n^1.5) | O(1) |
插入类排序算法稳定性:
排序算法 | 稳定性 | 证明 |
---|---|---|
直接插入排序 | 稳定 | 排序从后向前进行,while(a[j]>key)保证了后面的元素不会排到前面 |
希尔排序 | 不稳定 | 反例,{2,4,1,2} |
2 交换排序
交换类排序的思想是通过一系列交换逆序元素进行排序的方法.
2.1 冒泡排序
通过对相邻的数据元素进行交换,逐步将待排序序列变成有序序列.
算法思想:
反复扫描待排序记录序列,在扫描的过程中顺次比较相邻的两个元素的大小,若逆序就交换位置.
在扫描的过程中,不断的将相邻两个记录中关键字大的记录向后移动,最后必然将待排序记录序列中的最大关键字记录换到待排序序列的末尾,这也是待排序记录应该在的位置.
然后进行第二趟冒泡排序,对前n-1个记录进行同样的操作,其结果是使次大的记录被放在n-1的位置上.
如此反复,每一趟冒泡排序都将一个记录排到位,直到剩下一个最小的记录.
若在某一趟冒泡排序过程中,没有发现一个逆序,则可直接结束整个排序过程,所以冒泡过程最多进行n-1趟.
public static void bubbleSort(int a[]) {
boolean change = true;
for (int i = 1; i <= a.length - 1 && change; i++) {
change = false;
for (int j = 0; j < a.length - i; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
change = true;
}
}
}
}
public static void main(String[] args) {
int a[] = {3, 1, 2, 5, 8, 6,88,11,55,22,33,99,76};
bubbleSort(a);
System.out.println(Arrays.toString(a));
}
2.2 快速排序
算法改进要点:
冒泡排序,在扫描过程中只对相邻的两个元素进行比较,因此在互换两个相邻元素时只能消除一个逆序.如果能通过两个不相邻的元素的交换,消除待排序序列中的多个逆序,则会大大加快排序速度.
快速排序方法中的一次交换可能消除多个逆序.
算法思想:
从待排序记录序列中选取一个记录(通常选取第一个记录,不太好的做法)为枢轴,其关键字设位K1,然后将其余关键字小于K1的记录移到前面,而将关键字大于K1的记录移到后面,结果将待排序记录分成两个子表,最后将关键字为K1的记录插入其分界线的位置处.将这个过程称为一趟快速排序.
通过一次划分后,就以关键字为K1的记录为界,将待排序序列分成了两个子表,且前面字表中所有记录的关键字都不大于K1,而后面子表所有关键字均不小于K1.
对分割后的子表继续按照上述原则进行分割,知道所有子表的表长不超过1,此时待排序序列就成了一个有序表.
算法步骤:
假设待划分序列为a[left],a[left+1],…,a[right],具体实现上述划分过程时,可以设两个指针i和j,他们的初值分别位left,right.
首先将基准记录a[left]移至变量key中,使a[left],即a[left]相当于空单元,然后反复进行如下两个扫描过程,直到i和j相遇.
- j从右向左扫描,直到a[j]
<
key,将a[j]移至空单元a[i],此时a[j]相当于空单元. - i从左向右扫描,直到a[i]
>
key,将a[i]移至空单元a[j],此时a[i]相当于空单元.
当i和j相遇时,a[i]相当于空单元,且a[i]左边的所有记录的关键字均不大于基准记录的关键字,而a[i]右边所有记录的关键字均不小于基准记录的关键字.
最后将基准记录移至a[i]中,就完成了一次划分过程.对于a[i]左边的子表和a[i]右边的子表可采用相同的方法进一步划分.
public static void quickSort(int a[], int low, int height) {
if (low < height) {
int pos = quickPass(a, low, height);
quickSort(a, low, pos - 1);
quickSort(a, pos + 1, height);
}
}
public static int quickPass(int a[], int low, int height) {
int key = a[low];
while (low < height) {
while (low < height && a[height] >= key) {
height--;
}
if (low < height) {
a[low] = a[height];
low++;
}
while (low < height && a[low] <= key) {
low++;
}
if (low < height) {
a[height] = a[low];
height--;
}
}
a[low] = key;
return low;
}
public static void main(String[] args) {
int a[] = {3, 1, 5, 9, 11, 6, 99, 22, 55};
quickSort(a, 0, a.length-1);
System.out.println(Arrays.toString(a));
}
2.3 小结
交换类排序算法:
排序算法 | 改进思路 | 时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|---|---|
冒泡排序 | 基本排序方法 | O(n^2) | O(n) | O(n^2) | O(1) |
快速排序 | 交换不相邻的两个元素,消除多个逆序 | O(nlog2n) | O(nlog2n) | O(n^2) | O(log2n) |
交换类排序算法稳定性:
排序算法 | 稳定性 | 证明 |
---|---|---|
冒泡排序 | 稳定 | 排序从前向后进行,while(a[j]>key)保证了后面的元素不会排到前面 |
快速排序 | 不稳定 | 反例,{3,3,2} |
3 选择排序
选择类排序的基本思想是:
每趟在n-i+1(i=1,2,3,…,n-1)个记录中选取关键字最小的记录作为有序记录中的第i个记录.
3.1 简单选择排序
算法思想:
第一趟简单选择排序时,从第一个记录开始,通过n-1次关键字的比较,从n个记录中选出关键字最小的记录,并和第一个记录进行交换.
….
….
第i趟时,从第i个记录开始,通过n-i次关键字的比较,从n-i+1个记录中选出最小的关键字,并和第i个记录进行交换.
经过n-1趟简单选择排序,将把n-1个记录排到位,剩下一个最小记录直接在最后,所以共需n-1趟简单选择排序.
public static void selectSort(int a[]) {
for (int i = 0; i < a.length - 1; i++) {
int k = i;
for (int j = i + 1; j < a.length; j++) {
if (a[k] > a[j]) {
k = j;
}
}
if (k != i) {
int temp = a[i];
a[i] = a[k];
a[k] = temp;
}
}
}
public static void main(String[] args) {
int a[] = {3, 1, 5, 9, 11, 6, 99, 22, 55};
selectSort(a);
System.out.println(Arrays.toString(a));
}
3.2 堆排序
采用堆排序时,只需要一个记录大小的辅助空间.堆排序实在排序过程中,将向量存储的数据看成一棵完全二叉树,利用完全二叉树中双亲结点和孩子结点之间的内在关系来选择关键最小记录,即待排序记录仍采用向量数组方式存储,并非采用树的存储结构,而仅仅是采用完全二叉树的顺序结构的特征进行分析而已.
算法思想:
把待排序的记录的关键字存放在数组a[0,n-1]中,将a看成一棵完全二叉树的顺序表示,每个结点表示一个记录,第一个记录a[0]表示二叉树的根,以下各记录a[1]~a[n-1]依次逐层从左到右排列,任意结点a[i]的左孩子是a[2i+1],右孩子是a[2i+2],双亲是a[i/2取整].
大根堆:
各个结点的关键字:a[i] >= a[2i+1]并且a[i] >= a[2i+2]的完全二叉树.
小根堆:
各个结点的关键字:a[i] < a[2i+1]并且a[i] < a[2i+2]的完全二叉树.
堆排序的过程主要需要解决两个问题:一是按照堆的规定,建初堆, 二是去掉最大元之后重建堆,得到次大元,以此类推.
重建堆
问题:当堆顶记录改变时,如何重建堆.
算法思想:
首先将与堆相应的完全二叉树根节点中的记录移处,该记录称为待调整记录.
此时根节点相当于空结点,从空结点的左右子树中选出一个关键字较大的记录,如果该记录大于待调整的关键字,则将该记录上移至空结点.
此时,原来那个关键字较大的子结点相当于空结点,从空结点的左右子树中选出一个较大的记录,如果该记录的关键字扔大于待调整结点的关键字,则将该记录上移至空结点中.
重复上述移动过程,直到空结点左右子树关键字均小于待调整记录的关键字.此时,将待调整记录放入空结点即可.
建初堆
问题:如何由一个任意序列建初堆?
算法思想:
将任意一个序列看成是对应的完全二叉树,由于叶结点可以视为单元素的堆,因此可以反复利用上述调整堆的算法,自底向上逐层把所有子树调整为堆,直到整个完全二叉树调整为堆.
在完全二叉树中,最后一个非叶结点位于n/2个位置,n为二叉树结点数目,因此,筛选需从2/n个结点开始,逐层向上倒退,直到根节点.
堆排序算法实现
问题:如何利用堆完成排序
算法思想:
1. 将待排序记录按照堆的定义建初堆.
2. 调整剩余的记录序列,将n-i个元素重新筛选为一个新堆
3. 重复2,n-1次.
package 排序.选择排序;
import java.util.Arrays;
/**
* https://zh.wikipedia.org/wiki/%E5%A0%86%E6%8E%92%E5%BA%8F
*/
public class HeapSort {
private int[] arr;
public HeapSort(int[] arr) {
this.arr = arr;
}
/**
* 堆排序的主要入口方法,共两步。
*/
public void sort() {
/*
* 第一步:将数组堆化
* beginIndex = 第一个非叶子节点。
* 从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
* 叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
*/
int len = arr.length - 1;
int beginIndex = (len - 1) / 2;
for (int i = beginIndex; i >= 0; i--) {
maxHeapify(i, len);
}
/*
* 第二步:对堆化数据排序
* 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1。
* 然后从新整理被换到根节点的末尾元素,使其符合堆的特性。
* 直至未排序的堆长度为 0。
*/
for (int i = len; i > 0; i--) {
swap(0, i);
maxHeapify(0, i - 1);
}
}
private void swap(int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* 调整索引为 index 处的数据,使其符合堆的特性。
*
* @param index 需要堆化处理的数据的索引
* @param len 未排序的堆(数组)的长度
*/
private void maxHeapify(int index, int len) {
int li = 2 * index + 1; // 左子节点索引
int ri = li + 1; // 右子节点索引
int cMax = li; // 子节点值最大索引,默认左子节点。
if (li > len) return; // 左子节点索引超出计算范围,直接返回。
if (ri <= len && arr[ri] > arr[li]) // 先判断左右子节点,哪个较大。
cMax = ri;
if (arr[cMax] > arr[index]) {
swap(cMax, index); // 如果父节点被子节点调换,
maxHeapify(cMax, len); // 则需要继续判断换下后的父节点是否符合堆的特性。
}
}
/**
* 测试用例
* <p>
* 输出:
* [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9]
*/
public static void main(String[] args) {
int[] arr = new int[]{3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6};
new HeapSort(arr).sort();
System.out.println(Arrays.toString(arr));
}
}
2.3 小结
选择类排序算法:
排序算法 | 改进思路 | 时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|---|---|
简单选择排序 | 基本排序方法 | O(n^2) | O(n^2) | O(n^2) | O(1) |
堆排序 | 将数组看成一棵完全二叉树,减少辅助空间 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) |
选择类排序算法稳定性:
排序算法 | 稳定性 | 证明 |
---|---|---|
简单选择排序 | 不稳定 | 反例 |
堆排序 | 不稳定 | 反例 |
4 归并排序
归并排序基本思想:
基于合并,将两个或两个以上有序表合并成一个新的有序表.
2路归并排序
算法思想:
假设初始序列含有n个记录,首先将这个n个记录看成n个有序子序列,每个序列的长度为1,然后两两归并,并得到n/2个长度为2的有序子序列.
再对长度为2的有序子序列进行两两归并.
如此重复,直到有序为止.
排序算法 | 时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|---|
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) |
选择类排序算法稳定性:
排序算法 | 稳定性 |
---|---|
归并排序 | 稳定 |
static void mergeSort(int[] a, int[] tmpArray, int left, int right) {
if (left < right) {
int center = (left + right) / 2;
mergeSort(a, tmpArray, left, center);
mergeSort(a, tmpArray, center + 1, right);
merge(a, tmpArray, left, center + 1, right);
}
}
static void mergeSort(int a[]) {
int[] tmpArray = new int[a.length];
mergeSort(a, tmpArray, 0, a.length - 1);
}
static void merge(int[] a, int[] tmpArray, int lefPos, int rightPos, int rightEnd) {
int leftEnd = rightPos - 1;
int tmpPos = lefPos;
int numElements = rightEnd - lefPos + 1;
while (lefPos <= leftEnd && rightPos <= rightEnd) {
if (a[lefPos] <= a[rightPos]) {
tmpArray[tmpPos++] = a[lefPos++];
} else {
tmpArray[tmpPos++] = a[rightPos++];
}
}
while (lefPos <= leftEnd) {
tmpArray[tmpPos++] = a[lefPos++];
}
while (rightPos <= rightEnd) {
tmpArray[tmpPos++] = a[rightPos++];
}
for (int i = 0; i < numElements; i++, rightEnd--) {
a[rightEnd] = tmpArray[rightEnd];
}
}
public static void main(String[] args) {
int a[] = {1, 3, 5, 7, 10, 4, 6, 8, 13, 15};
mergeSort(a);
System.out.println(Arrays.toString(a));
}