八大排序介绍
稳定与非稳定:
如果一个排序算法能够保留数组中重复元素的相对位置则可以被称为是稳定的。反之,则是非稳定的。
基本思想原理:顺序把待排序的数据元素按其关键字值的大小插入到已排序数据元素子集合的适当位置。
算法描述:
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
代码示例
public class InsortSort {
public static void sort(int[] a) {
for (int i = 1; i < a.length; i++) {
int num = a[i];
int j;
for (j = i; j > 0 && num < a[j - 1]; j--) {
a[j] = a[j - 1];
}
a[j] = num;
}
}
public static void main(String[] args) {
int[] arr = {1, 3, 2, 5};
System.out.println("排序前:"+ Arrays.toString(arr));
sort(arr);
System.out.println("排序后:"+ Arrays.toString(arr));
}
}
复杂度分析
比较与总结
插入排序所需的时间取决于输入元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比随机顺序的数组或是逆序数组进行排序要快得多。
也称 递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。
基本思想原理:将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
希尔排序是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
算法描述:
代码示例
public class ShellSort {
public static void sort(int[] arr) {
//增量每次都/2
for (int step = arr.length / 2; step > 0; step /= 2) {
//从增量那组开始进行插入排序,直至完毕
for (int i = 0; i < arr.length - 1; i++) {
int j;
int temp = arr[i];
// j - step 就是代表与它同组隔壁的元素
for (j = i; j - step > 0 && arr[j - step] > temp; j = j - step) {
arr[j] = arr[j - step];
}
arr[j] = temp;
}
}
}
public static void main(String[] args) {
int[] arr = {1,4,2,6,5,7};
System.out.println("排序前:"+ Arrays.toString(arr));
sort(arr);
System.out.println("排序后:"+ Arrays.toString(arr));
}
}
复杂度分析
总结与思考
希尔排序更高效的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。
基本思想原理:每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止。
算法描述:
代码示例
public class SelectionSort {
public static void sort(int[] arr) {
for(int i = 0; i < arr.length - 1; i++) {// 做第i趟排序
int k = i;
for(int j = k + 1; j < arr.length; j++){// 选最小的记录
if(arr[j] < arr[k]){
k = j; //记下目前找到的最小值所在的位置
}
}
//在内层循环结束,也就是找到本轮循环的最小的数以后,再进行交换
if(i != k){ //交换a[i]和a[k]
int temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
}
}
}
public static void main(String[] args) {
int[] arr={1,3,2,45,65,33,12};
System.out.println("排序前:"+ Arrays.toString(arr));
sort(arr);
System.out.println("排序后:"+ Arrays.toString(arr));
}
}
复杂度分析
总结与思考
选择排序的简单和直观名副其实,这也造就了它”出了名的慢性子”,无论是哪种情况,哪怕原数组已排序完成,它也将花费将近n²/2次遍历来确认一遍。即便是这样,它的排序结果也还是不稳定的。 唯一值得高兴的是,它并不耗费额外的内存空间。
1991年的计算机先驱奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德(Robert W.Floyd) 和威廉姆斯(J.Williams) 在1964年共同发明了著名的堆排序算法(Heap Sort).
堆的定义如下:nn个元素的序列{k1,k2,..,kn}
当且仅当满足下关系时,称之为堆。
把此序列对应的二维数组看成一个完全二叉树。那么堆的含义就是:完全二叉树中任何一个非叶子节点的值均不大于(或不小于)其左,右孩子节点的值。 由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。因此我们可使用大顶堆进行升序排序, 使用小顶堆进行降序排序。
基本思想原理:子结点的键值或索引总是小于(或者大于)它的父节点
此处以大顶堆为例,堆排序的过程就是将待排序的序列构造成一个堆,选出堆中最大的移走,再把剩余的元素调整成堆,找出最大的再移走,重复直至有序。
算法思路:
代码示例
public class HeapSort {
public static void main(String[] args) {
int[] a = {1, 3, 2, 9, 6, 8};
System.out.println("排序前:"+ Arrays.toString(a));
sort(a);
System.out.println("排序后:"+ Arrays.toString(a));
}
public static void sort(int[] a) {
for (int i = a.length - 1; i > 0; i--) {
max_heapify(a, i);
//堆顶元素(第一个元素)与Kn交换
int temp = a[0];
a[0] = a[i];
a[i] = temp;
}
}
/***
* 将数组堆化
* i = 第一个非叶子节点。
* 从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
* 叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
*/
public static void max_heapify(int[] a, int n) {
int child;
for (int i = (n - 1) / 2; i >= 0; i--) {
//左子节点位置
child = 2 * i + 1;
//右子节点存在且大于左子节点,child变成右子节点
if (child != n && a[child] < a[child + 1]) {
child++;
}
//交换父节点与左右子节点中的最大值
if (a[i] < a[child]) {
int temp = a[i];
a[i] = a[child];
a[child] = temp;
}
}
}
}
复杂度分析
总结与思考
由于堆排序中初始化堆的过程比较次数较多, 因此它不太适用于小序列。 同时由于多次任意下标相互交换位置, 相同元素之间原本相对的顺序被破坏了, 因此, 它是不稳定的排序。
一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
基本原理思想:比较两个相邻的元素,将值大的元素交换至右端。
算法描述
代码示例
public class BubbleSort {
public static int[] sort(int[] array) {//外层循环控制排序趟数
for (int i = 0; i < array.length - 1; i++) {//内层循环控制每一趟排序多少次
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}
return array;
}
public static void main(String[] args) {
int[] arr = {6, 3, 8, 2, 9, 1};
System.out.println("排序前:"+ Arrays.toString(arr));
sort(arr);
System.out.println("排序后:"+ Arrays.toString(arr));
}
}
复杂度分析
冒泡排序是最容易实现的排序, 最坏的情况是每次都需要交换, 共需遍历并交换将近n²/2次, 时间复杂度为O(n²). 最佳的情况是内循环遍历一次后发现排序是对的, 因此退出循环, 时间复杂度为O(n). 平均来讲, 时间复杂度为O(n²). 由于冒泡排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1).
总结与思考
由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法。
快速排序属于分治法的一种,就是说通过把数据分成几部分来同时处理的一种算法。
基本思想原理:每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于基准点的数全部放到基准点的右边。
算法思想:
以6 1 2 7 9 3 4 5 10 8为例,通过图解得知其排序过程
第1步:找基准值:选取数组的第一个数为基准值
第2步:比大小
第3步:交换
第4步:继续查找
第5步:交换基准值到合适的位置
第6步:重复:基准值左右两边的两个子数组重复前面五个步骤直至排序完成
代码示例
public class QuickSort {
public static void quickSort(int[] array) {
int len;
if (array == null || (len = array.length) == 0 || len == 1) {
return;
}
sort(array, 0, len - 1);
}
//@todo 快排核心算法,分治法,递归实现
public static void sort(int[] array, int left, int right) {
if (left > right) {
return;
}
//base中存放基准数
int base = array[left];
int i = left, j = right;
while (i != j) {
//顺序很重要,先从右边开始往左找,直到找到比base值小的数
while (array[j] >= base && i < j) {
j--;
}
//再从左往右边找,直到找到比base值大的数
while (array[i] <= base && i < j) {
i++;
}
//上面的循环结束表示找到了位置或者(i>=j)了,交换两个数在数组中的位置
if (i < j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
//将基准数放到中间的位置(基准数归位)
array[left] = array[i];
array[i] = base;
//递归,继续向基准的左右两边执行和上面同样的操作
//i的索引处为上面已确定好的基准值的位置,无需再处理
sort(array, left, i - 1);
sort(array, i + 1, right);
}
public static void main(String[] args) {
int[] arr = {6, 1, 2, 7, 9, 3, 4, 5, 10, 8};
System.out.println("排序前:"+ Arrays.toString(arr));
quickSort(arr);
System.out.println("排序后:"+ Arrays.toString(arr));
}
}
复杂度分析
归并排序是建立在归并操作上的一种有效的排序算法,1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
基本思想原理:
归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
算法描述:
代码示例
public class MergeSort {
//归并排序
public static int[] sort(int[] array, int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
sort(array, low, mid);
sort(array, mid + 1, high);
merge(array, low, mid, high);//归并
}
return array;
}
private static void merge(int[] array, int low, int mid, int high) {
int i = low;//指针,前一个序列的头指针
int j = mid + 1;//指针,后一个序列的头指针
int[] temp = new int[high - low + 1];
int k = 0;
while (i <= mid && j <= high) {
if (array[i] < array[j]) {//从头比较两个序列,小的放入临时数组temp
temp[k++] = array[i++];//前一个序列指针后移一位
} else {
temp[k++] = array[j++];//后一个序列指针后移一位
}
}
//最后只会剩下一组序列
while (i <= mid) {
temp[k++] = array[i++];//把前一个指针剩余的数字放入临时数组
}
while (j <= high) {
temp[k++] = array[j++];//把后一个指针剩余的数字放入临时数组
}
for (int m = 0; m < high - low + 1; m++) {
array[low + m] = temp[m];
}
}
public static void main(String[] args) {
int[] arr = {1, 3, 4, 2, 6, 9, 7, 5};
System.out.println("排序前:" + Arrays.toString(arr));
sort(arr, 0, arr.length - 1);
System.out.println("排序后:" + Arrays.toString(arr));
}
}
复杂度分析
从效率上看,归并排序可算是排序算法中的”佼佼者”. 假设数组长度为n,那么拆分数组共需logn,, 又每步都是一个普通的合并子数组的过程, 时间复杂度为O(n), 故其综合时间复杂度为O(nlogn)。另一方面, 归并排序多次递归过程中拆分的子数组需要保存在内存空间, 其空间复杂度为O(n)。
总结与思考
归并排序最吸引人的性质是它能够保证将任意长度为N的数组排序所需时间和NlogN成正比,它的主要缺点则是他所需的额外空间和N成正比。
基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine), 排序器每次只能看到一个列。它是基于元素值的每个位上的字符来排序的。 对于数字而言就是分别基于个位,十位, 百位或千位等等数字来排序。
基本思想原理:是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
算法描述:
我们以LSD为例,从最低位开始,具体算法描述如下:
代码示例
public class BaseSort {
public static void sort(int[] arr) {
if (arr.length <= 1) return;
//取得数组中的最大数,并取得位数
int max = 0;
for (int i = 0; i < arr.length; i++) {
if (max < arr[i]) {
max = arr[i];
}
}
int maxDigit = 1;
while (max / 10 > 0) {
maxDigit++;
max = max / 10;
}
//申请一个桶空间
int[][] buckets = new int[10][arr.length];
int base = 10;
//从低位到高位,对每一位遍历,将所有元素分配到桶中
for (int i = 0; i < maxDigit; i++) {
int[] bktLen = new int[10]; //存储各个桶中存储元素的数量
//分配:将所有元素分配到桶中
for (int j = 0; j < arr.length; j++) {
int whichBucket = (arr[j] % base) / (base / 10);
buckets[whichBucket][bktLen[whichBucket]] = arr[j];
bktLen[whichBucket]++;
}
//收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞
int k = 0;
for (int b = 0; b < buckets.length; b++) {
for (int p = 0; p < bktLen[b]; p++) {
arr[k++] = buckets[b][p];
}
}
base *= 10;
}
}
public static void main(String[] args) {
int[] arr = {1, 3, 2, 7, 9, 5, 4, 67, 12, 45, 56};
System.out.println("排序前:" + Arrays.toString(arr));
sort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
}
}
复杂度分析
其中,d 为位数,r 为基数,n 为原数组个数。在基数排序中,因为没有比较操作,所以在复杂上,最好的情况与最坏的情况在时间上是一致的,均为 O(d*(n + r))
。
总结和思考
基数排序更适合用于对时间, 字符串等这些 整体权值未知的数据 进行排序。
基数排序不改变相同元素之间的相对顺序,因此它是稳定的排序算法。
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
Arrays.sort() 采用了2种排序算法 -- 基本类型数据使用快速排序法,对象数组使用归并排序。
Collections.sort算法调用的是归并排序。