1.排序算法简介
由于待排序的记录数量不同,使得排序过程中涉及的存储器不同,可以将排序方法分为两大类:内部排序和外部排序。
内部排序:待排序记录存放在计算机的随机存储器(内存)中进行的排序过程。内部排序的方法很多,但就其全面性能而言,很难提出一种被认为最好的方法。每一种方法都有自己的优缺点,适合在不同的环境下使用(比如记录的初始排列状态等)。按照排序过程中依据的不同原则对内部排序大概可以分为以下几类:插入排序,交换排序,选择排序,归并排序和基数排序。
外部排序:待排序的记录数量很大,内存一次不能容纳全部记录,在排序过程中需要对外存进行访问的排序过程。(内存和外存同时使用)
各种排序算法之间的关系罗列:
2.各种排序算法介绍与实例
2.1直接插入排序
算法概念:每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表任然有序。
算法思想:假设待排序的记录存放在数组R[1..n]中。初始时,R[1]自成1个有序区,无序区为R[2..n]。从i=2起直至i=n为止,依次将R[i]插入当前的有序区R[1..i-1]中,生成含n个记录的有序区。
算法示例:
//(直接)插入排序,时间复杂度O(n^2),稳定
public class InsertSort {
public static void main(String[] args) {
int[] nums = {5,3,2,7,1,4,6};
startSort(nums);
for (int i = 0; i < nums.length - 1; i++) {
System.out.println(nums[i]);
}
}
public static void startSort(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
int j = i + 1;
//temp中存放需要插入的元素
int temp = nums[j];
//temp为待插入的元素,和已经有序列表的最后一个数(最大数)进行比较
//若max>temp,max向后移动,temp再和前面的次最大数比较...
//若max<temp,temp直接插入在最后即可
while(i >= 0 && nums[i] > temp) {
nums[j] = nums[i];
i --;
j --;
}
nums[j] = temp;
}
}
}
算法总结:时间复杂度O(n^2),是稳定的排序方式。
2.2希尔排序(缩小增量排序)
算法概念:也是一种插入排序的算法,但在时间效率上有较大的改进。
算法思想:
(a)先将整个待排序记录分割成若干个子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
(b)先取一个小于n(n为数组长度)的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插人排序;然后,取第二个增量d2(其中d2小于d1)重复上述的分组和排序,直至所取的增量dt=1(dt<dt-1<…<d2<d1)
,即所有记录放在同一组中进行直接插入排序为止。
算法示例:
//希尔排序,时间复杂度O(nlogn),不稳定
public class ShellSort {
public static void main(String[] args) {
int[] nums = {5,3,2,7,1,4,6};
startSort(nums);
for (int i = 0; i < nums.length - 1; i++) {
System.out.println(nums[i]);
}
}
public static void startSort(int[] nums) {
// 分成2组
int len = nums.length / 2;
while (len >= 1) {
for (int i = 0; i < len; i++) {
// 使用直接插入排序对分组进行排序
for (int j = i; j < nums.length - len; j += len) {
int k = j + len;
int temp = nums[k];
while (j >= 0 && nums[j] > temp) {
nums[k] = nums[j];
j -= len;
k -= len;
}
nums[k] = temp;
}
}
len /= 2;
}
}
}
算法总结:时间复杂度O(nlogn),不稳定。
2.3简单选择排序
算法思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数中再找出最小的数与第二个位置的数交换,如此循环到倒数第二个数,将该数与最后一个数比较,较小的一个放在前一个位置。
算法示例:
//选择排序,时间复杂度O(n^2),不稳定
public class SelectSort {
public static void main(String[] args) {
int[] nums = { 5, 3, 2, 7, 1, 4, 6 };
startSort(nums);
for (int i = 0; i < nums.length - 1; i++) {
System.out.println(nums[i]);
}
}
public static void startSort(int[] nums) {
// 标志位
int position = 0;
for (int i = 0; i < nums.length; i++) {
int j = i + 1;
position = i;
int temp = nums[i];
// 找出后面数中的最小者
for (; j < nums.length; j++) {
if (nums[j] < temp) {
temp = nums[j];
position = j;
}
}
nums[position] = nums[i];
nums[i] = temp;
}
}
}
算法总结:时间复杂度O(n^2),不稳定。
2.4堆排序
算法思想:堆排序是一种用完全二叉树解决问题的高效算法,合法的最大堆就是每一个节点值都要大于或等于它的孩子节点,在数组中可表示为:(i从0开始)arrays[i]>=arrays[2*i+1] && arrays[i]>=arrays[2*i+2]
,最小堆的概念和最大堆相反,此处采用最大堆。
堆排序树的构造过程找最大值过程由下图,数组arrays[0….n]为:17,8,45,84,2,94,刚找到最大值后把最大值即94放在数组的最后面arrays[n],然后进入递归把arrays[0…n-1]再进入下面图这个过程,只是把排好序的最大值不放入到这个过程中,就这样把值一个个的冒出来,找到最大值后把这个最大值放到数组的最后面,进入下一个递归。
注意:每次都要扫描整个数组,最先发现不符合最大堆的先调整。
算法示例:
//堆排序,时间复杂度O(nlogn),不稳定
public class HeapSort {
public static void main(String[] args) {
int[] nums = {5,3,2,7,1,4,6};
int e = nums.length - 1;
nums = startSort(nums,e);
for (int i = 0; i < nums.length - 1; i++) {
System.out.println(nums[i]);
}
}
// 堆排序
public static int[] startSort(int[] nums, int e) {
// 堆中的元素多于1个
if (e > 0) {
initHeap(nums, e);// 初始化堆
// 在一轮的堆排序中,找到了最大值,将最大值与最后一个元素交换,重新开始下一轮
nums[0] = nums[e] + nums[0];
nums[e] = nums[0] - nums[e];
nums[0] = nums[0] - nums[e];
// 递归开始下一轮
startSort(nums, e - 1);
} else {
// 堆中不多于1个元素,不需要进行堆排序
return nums;
}
return nums;
}
// 初始化堆
public static void initHeap(int[] nums, int e) {
int m = (e + 1) / 2; // 父节点的个数
for (int i = 0; i < m; i++) {
boolean flag = buildHeap(nums, e, i);
// 如果父节点和孩子节点之间有交换,需要重新扫描整个树中的父节点
if (flag) {
i = -1;
}
}
}
// 建立堆
public static boolean buildHeap(int[] nums, int e, int i) {
int l_child = 2 * i + 1;// 左孩子
int r_child = 2 * i + 2;// 右孩子
// 没有右孩子的时候只需要比较父节点和左孩子节点的大小
if (r_child > e) {
// 父节点比左孩子节点小,需要交换
if (nums[i] < nums[l_child]) {
// 不使用中间变量交换父节点和左孩子节点的值
nums[i] = nums[i] + nums[l_child];
nums[l_child] = nums[i] - nums[l_child];
nums[i] = nums[i] - nums[l_child];
return true;
} else {
return false;
}
}
// 同时拥有左右两个孩子节点的情况,在三者中找出最大值进行交换
if (nums[i] < nums[l_child]) {
// 父节点比左孩子节点小
if (nums[l_child] > nums[r_child]) {
// 左孩子节点是三者中最大的,和父节点交换
nums[i] = nums[i] + nums[l_child];
nums[l_child] = nums[i] - nums[l_child];
nums[i] = nums[i] - nums[l_child];
return true;
} else {
// 右孩子节点是三者中最大的,和父节点交换
nums[i] = nums[i] + nums[r_child];
nums[r_child] = nums[i] - nums[r_child];
nums[i] = nums[i] - nums[r_child];
return true;
}
} else if (nums[i] < nums[r_child]) {
// 父节点比右孩子节点小
// 右孩子节点是三者中最大的,和父节点交换
nums[i] = nums[i] + nums[r_child];
nums[r_child] = nums[i] - nums[r_child];
nums[i] = nums[i] - nums[r_child];
return true;
}
// 父节点最大,不需要交换
return false;
}
}
算法总结:时间复杂度O(nlogn),不稳定。
2.5冒泡排序
算法思想:在要排序的一组数中,将第一个记录的关键字与第二个记录的关键字进行比较,让大的往下沉,小的往上冒,然后再比较第二个和第三个,以此类推。比较完一趟,最大的那个已经放到了最后的位置,这样可以对剩下的n-1个再循环比较。
算法示例:
//冒泡排序,时间复杂度O(n^2),稳定
public class BubbleSort {
public static void main(String[] args) {
int[] nums = {5,3,2,7,1,4,6};
startSort(nums);
for (int i = 0; i < nums.length - 1; i++) {
System.out.println(nums[i]);
}
}
public static void startSort(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
for (int j = 0; j < nums.length - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
}
}
算法总结:算法复杂度为O(n^2),稳定的。
2.6快速排序
算法思想:选择一个基准元素,通常选择第一个元素或者最后一个元素,这里选择第一个元素为基准。一个指向基准元素且整个过程指针不变,一个指向最后一个元素,两者比较,前面的元素大于后面的元素则交换,非基准元素的指针向中心移动一位,继续上述步骤,通过一趟扫描,将待排序列分成两个部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素的位置是排好序后的正确位置,然后再用同样的方法递归地排序两个部分。
快速排序是对冒泡排序的一种改进。
算法示例:
//快速排序,时间复杂度O(nlogn),不稳定
public class QuickSort {
public static void main(String[] args) {
int[] nums = {5,3,2,7,1,4,6};
int low = 0;
int high = nums.length - 1;
nums = startSort(nums,low,high);
for (int i = 0; i < nums.length - 1; i++) {
System.out.println(nums[i]);
}
}
public static int[] startSort(int[] nums,int low,int high) {
//递归的终止条件是low >= high
if(low < high) {
int middle = getMiddle(nums,low,high);
//对基准元素前面部分递归排序
startSort(nums,low,middle-1);
//对基准元素后面部分递归排序
startSort(nums,middle+1,high);
}
return nums;
}
public static int getMiddle(int[] nums, int low,int high) {
//选择基准元素
int temp = nums[low];
//两个指针都向中心处移动,直至相等
while (low < high) {
//右指针的元素总是比基准元素大时
while (low < high && nums[high] >= temp) {
high --;
}
//比基准元素小的移动到左侧
nums[low] = nums[high];
//左指针的元素总是比基准元素小时
while (low <high && nums[low] <= temp) {
low ++;
}
//比基准元素大的移动到右侧
nums[high] = nums[low];
}
nums[low] = temp;
return low;
}
}
算法总结:时间复杂度O(nlogn),不稳定。
2.7归并排序
算法思想:将两个(或者两个以上)的有序表合并成一个新的有序表,即把待排序序列分成若干个子序列,每个子序列都是有序的,然后再把有序子序列合并为整体有序序列。
算法在排序的过程中会将整个序列分成两个数为一单元的组合,然后设定两个指针,最初位置分别为两个已经排序序列(一开始两个单独的数即为两个已经排序的序列)的起始位置,然后比较两个指针所指向的元素,选择相对小的元素放入到临时空间,并移动指针到下一位置,直到某一指针移动到序列尾,将另一序列剩下的所有元素直接复制到合并序列的尾部。
算法示例:
//归并排序,时间复杂度O(nlogn),稳定
public class MergeSort {
public static void main(String[] args) {
int[] nums = {5,3,2,7,1,4,6};
int low = 0;
int high = nums.length - 1;
startSort(nums,low,high);
for (int i = 0; i < nums.length - 1; i++) {
System.out.println(nums[i]);
}
}
// 归并排序
public static int[] startSort(int[] nums, int low, int high) {
// 找出中间的索引
int mid = (low + high) / 2;
if (low < high) {
// 对左边数组进行递归
startSort(nums, low, mid);
// 对右边数组进行递归
startSort(nums, mid + 1, high);
// 左右归并
merge(nums, low, mid, high);
}
return nums;
}
// 将左右两个有序的子序列进行合并
public static void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 把较小的数先移动到新数组中
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = nums[i++];
}
// 把右边剩余的数移入数组
while (j <= high) {
temp[k++] = nums[j++];
}
// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}
}
算法总结:算法复杂度O(nlogn),稳定。
2.8基数排序
算法思想:将所有待比较值(正整数)统一为同样的数位长度,数位较短的数前面补0,然后从最低位开始,进行依次排序。这样从最低位一直到最高位排序完成以后,数列就变成了一个有序的序列。
在众多的排序方法中,基数排序比较特殊,它是一种不需要进行关键字之间比较的排序方法,利用多关键字的划分,逐渐实现有序。
比如:278,109,63,930,589,184,505,269,8,83进行排序的过程如下:
(a)第一次分组,每一个元素每一位上的数值都是0~9,所以划分为10组,按照每个元素个位上的数值进行分组:
0组:930
1组:
2组:
3组:63,83
4组:184
5组:505
6组:
7组:
8组:278,8
9组:109,589,269
第一次排序后的结果:930,63,83,184,505,278,8,109,589,269
(b)第二次分组,将第一次分组后的结果按照十位上的数进行分组:
0组:505,8,109
1组:
2组:
3组:930
4组:
5组:
6组:63,269
7组:278
8组:83,184,589
9组:
第二次排序后的结果:505,8,109,930,63,269,278,83,184,589
(c)第三次分组,将第二次分组后的结果按照百位上的数进行分组:
0组:8,63,83
1组:109,184
2组:278,269
3组:
4组:
5组:505,589
6组:
7组:
8组:
9组:930
第三次排序后的结果:8,63,83,109,184,278,269,505,589,930
(d)最高只有百位,没有千位,排序结束,输出结果。
算法示例:
import java.util.ArrayList;
import java.util.List;
//基数排序,时间复杂度O(nlogrm),稳定
public class RadixSort {
public static void main(String[] args) {
int[] nums = {5,3,2,7,1,4,6};
nums = startSort(nums);
for (int i = 0; i < nums.length - 1; i++) {
System.out.println(nums[i]);
}
}
// 基数排序
public static int[] startSort(int[] nums) {
int max = nums[0];
// 选出数列中的最大值
for (int i = 1; i < nums.length; i++) {
if (nums[i] > max) {
max = nums[i];
}
}
// 计算总共需要比较多少次
// 注意此处小于10的正整数除以10结果为0
int time = 0;
while (max > 0) {
max /= 10;
time++;
}
// 设定10个list保存数据
List<ArrayList<Integer>> queue = new ArrayList<ArrayList<Integer>>();
for (int j = 0; j < 10; j++) {
ArrayList<Integer> dataQueue = new ArrayList<Integer>();
queue.add(dataQueue);
}
// 开始排序
for (int i = 0; i < time; i++) {
for (int j = 0; j < nums.length; j++) {
// 取到当前这个数的对应位置上的一位数
int x = nums[j] % (int) Math.pow(10, i + 1)
/ (int) Math.pow(10, i);
ArrayList<Integer> returnQueue = queue.get(x);
returnQueue.add(nums[j]);
queue.set(x, returnQueue);
}
int count = 0;
for (int k = 0; k < 10; k++) {
while (queue.get(k).size() > 0) {
ArrayList<Integer> tempQueue = queue.get(k);
nums[count] = tempQueue.get(0);
tempQueue.remove(0);
count++;
}
}
}
return nums;
}
}
算法总结:时间复杂度O(nlogrm),稳定。
3.各种排序算法的比较
排序算法 | 平均时间复杂度 | 最差时间复杂度 | 稳定性 | 空间复杂度 | 备注 |
---|---|---|---|---|---|
插入 | O(n^2) | O(n^2) | 稳定 | O(1) | 基本有序 |
希尔 | O(nlogn) | O(ns) | 不稳定 | O(1) | s是所选分组 |
选择 | O(n^2) | O(n^2) | 不稳定 | O(1) | n小的时候 |
堆 | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大的时候 |
冒泡 | O(n^2) | O(n^2) | 稳定 | O(1) | n小的时候 |
快速 | O(nlogn) | O(n^2) | 不稳定 | O(nlogn) | n大的时候 |
归并 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大的时候 |
基数 | O(logRB) | O(logRB) | 稳定 | O(n) | B是对数(0-9),R是基数(个十百) |
总结:没有哪一种排序算法是绝对最优的,要看具体的应用场景。