原文链接:十大经典排序算法总结(JavaScript描述)
排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
内排序:所有排序操作都在内存中完成;
外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
时间复杂度: 一个算法执行所耗费的时间。
空间复杂度: 运行完一个程序所需内存的大小。
n:数据规模
k:“桶”的个数
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。
它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
冒泡排序动图演示:
/**
* 从小到大
* asc
* 升序
* @param arr
*/
private static void bubbleSortAsc(int[] arr) {
int len = arr.length;
for (int i = 0; i < len-1; i++) {
for (int j = 0; j < len-1-i; j++) {
if(arr[j] > arr[j+1]){
//交换
int temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
}
改进
设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。
/**
* 冒泡排序改进
* @param arr
*/
public static void bubbleSortAscImprove(int[] arr){
int len = arr.length;
int i = len-1-0;
while(i > 0){
int pos = 0;
for (int j = 0; j < i; j++) {
if(arr[j] > arr[j+1]){
pos = j;//记录最后一次交换的位置。显然,在此位置之后的一定都已经是排好序了的
//交换
int temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
}
i = pos;
}
}
最佳情况:T(n) = O(n)
当输入的数据已经是正序时
最差情况:T(n) = O(n²)
当输入的数据是反序时
平均情况:T(n) = O(n²)
选择排序(Selection sort)是一种简单直观的排序算法。
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
选择排序动图演示:
/**
* 选择排序
* asc
* 升序
* @param arr
*/
public static void SelectSortAsc(int[] arr){
int len = arr.length;
for (int i = 0; i < len-1; i++) {
int minIndex = i;
for (int j = i+1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;//标记最小值的下标
}
}
//交换
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据。
/**
* 插入排序
* asc
* 升序
* @param arr
*/
public static void insertionSortAsc(int[] arr){
int len = arr.length;
for (int i = 0; i < len-1; i++) {
//已排序序列[0,i]
int key = arr[i+1];//待插入元素
int j = i;
while(j >= 0 && arr[j] > key) {
arr[j+1] = arr[j];//往后挪
j--;//从后向前扫描
}
arr[j+1] = key;//插入到下一个位置
}
}
改进
二分法
/**
* 二分插入排序
* asc
* 升序
* @param arr
*/
public static void binaryInsertionSortAsc(int[] arr){
int len = arr.length;
for (int i = 0; i < len-1; i++) {
//已排序序列[0,i]
int key = arr[i+1];//待插入元素
int left = 0;
int right = i;
while (left <= right) {
int middle = (left + right)/2;
if(key < arr[middle]){//
right = middle - 1;
} else {
left = middle + 1;
}
}
//把left及之后的值往右挪一个位置
for (int j = i; j >= left ; j--) {
arr[j+1] = arr[j];
}
arr[left] = key;
}
}
最佳情况:T(n) = O(n)
当输入的数据已经是正序时
最差情况:T(n) = O(n²)
当输入的数据是反序时
平均情况:T(n) = O(n²)
说明:
希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
/**
* 希尔排序
* asc
* 升序
* @param arr
*/
public static void shellSortAsc(int[] arr){
int len = arr.length;
int d = len/2;//设置第一个增量
if(d < 1)
d = 1;
while (true) {//增量d
//分组
for (int i = 0; i < d; i++) {//[0,0+d,0+d+d,...],[1,1+d,1+d+d,...],...[i,i+d,i+d+d,...],...[d-1,d-1+d,d-1+d+d,...]
for (int j = i; j < len-1; j+=d) {
//已排好序的是[i,j]
int key = arr[j+1];//带插入元素
int k = j;
while (k >= i && arr[k] > key) {
arr[k+1] = arr[k];
k--;
}
arr[k+1] = key;
}
}
if(d == 1)
break;
d = d/2 - 1;//尽量保证相邻的增量直接互不为倍数关系
if(d<1)
d = 1;
}
}
说明:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
归并操作的工作原理如下:
/**
* 归并排序
* asc
* 升序
* @param arr
*/
public static void mergingSortAsc(int[] arr){
int len = arr.length;
if (len < 2) {
return;
}
int middle = len/2;//把长度为n的输入序列分成两个长度为n/2的子序列;
mergeAsc(arr,0,middle,len);
}
/**
* 将子序列[left,middle)与子序列[middle,right)归并排序
* @param arr
* @param left
* @param middle
* @param right
*/
private static void mergeAsc(int[] arr, int left, int middle, int right) {
if ( (middle - left)==1 && (right - middle)==1 ){
//左右各为1
if(arr[left] > arr[middle]){
//交换
int temp = arr[middle];
arr[middle] = arr[left];
arr[left] = temp;
}
return;
}
/**
* 对这两个子序列分别采用归并排序;
*/
//先将子序列1归并排序
if( (middle - left) > 1){
int m_mid = (middle + left) / 2;
mergeAsc(arr, left, m_mid, middle);
}
//再将子序列2归并排序
if ( (right - middle) > 1 ){
int m_mid = (right + middle) / 2;
mergeAsc(arr, middle, m_mid, right);
}
//归并操作
int[] is = new int[right - left];//申请空间
int k = 0;
//设定两个指针
int m = left;
int n = middle;
while (m < middle && n < right) {
if(arr[m] > arr[n])
is[k++] = arr[n++];
else
is[k++] = arr[m++];
}
while (m < middle)
is[k++] = arr[m++];
while (n < right)
is[k++] = arr[n++];
for (int i = 0; i < is.length; i++) {
arr[left+i] = is[i];
}
}
改进
归并算法的思路是将序列分成两个子序列,子序列再分成两个子序列。然而序列的长度不一定是2的整数,所以可能会出现两个子序列的长度分别为1和2(这时长度为2的则需要再分成两个子序列),且这种情况极有可能有很多。
如果采用反向思维,第一趟把序列的第一个和第二个元素采用归并排序,第三个和第四个采用归并排序,重复直到将序列拆分成多个长度为2的有序子序列(若最后一个元素为单个,则将最后一个元素与最后一个子序列归并)。第二趟将每两个子序列进行归并排序,得到多个长度为4的有序子序列。重复直到只有两个有序子序列,再将这两个有序子序列归并排序。
代码如下:
/**
* 归并排序
* 改进
* asc
* 升序
* @param arr
*/
public static void mergingSortAscImprove(int[] arr){
mergeSortAscImprove(arr, 1);
}
/**
*
* @param arr
* @param size 从第一个元素开始,依次将相邻两个将长度为size的子序列合并成长度为2*size的序列
*/
private static void mergeSortAscImprove(int[] arr, int size){
int len = arr.length;
int sum = len/(size*2);
if(sum <= 0){//不能再拆分了
return ;
}
for (int i = 0; i < sum; i++) {
int left = i*size*2;
mergeAscImprove(arr,left , left+size, left+size*2);
}
int c = len%(size*2);//余数
if (c != 0){//余数与最后一个有序子序列归并排序。(如果最后一个也是第一个,那么这趟排序完整个排序就结束了。下次递归得到的sum==0)
mergeAscImprove(arr, len-c-size*2, len-c, len);
}
mergeSortAscImprove(arr,2*size);
}
/**
* 归并排序(子序列已经是有序的)
* @param arr
* @param left
* @param middle
* @param right
*/
private static void mergeAscImprove(int[] arr, int left, int middle, int right) {
//归并操作
int[] is = new int[right - left];//申请空间
int k = 0;
//设定两个指针
int m = left;
int n = middle;
while (m < middle && n < right) {
if(arr[m] > arr[n])
is[k++] = arr[n++];
else
is[k++] = arr[m++];
}
while (m < middle)
is[k++] = arr[m++];
while (n < right)
is[k++] = arr[n++];
for (int i = 0; i < is.length; i++) {
arr[left+i] = is[i];
}
}
快速排序(Quicksort)是对冒泡排序的一种改进。
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
具体算法如下:
/**
* 快速排序
* asc
* 升序
* @param arr
*/
public static void quickSortAsc(int[] arr){
int len = arr.length;
int i = 0;
int j = len-1;
quickSortAscRe(arr,i,j);
}
/**
* 递归
* 升序
* @param arr
* @param low
* @param high
*/
private static void quickSortAscRe(int[] arr, int low, int high) {
int i = low;
int j = high;
int key = arr[i];
while (i < j) {
//从j开始由后往前找到第一个小于key的数
while(i < j && arr[j] >= key){
j--;
}
if (i < j) {//找到了
//交换
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
//从i开始由前往后找到第一个大于key(此时key值对应的下边为j)的值
while (i < j && arr[i] <= key) {
i++;
}
if (i < j) {//找到了
//交换
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
//key又跑到i这里了
}
}
if (i > low) {
quickSortAscRe(arr, low, i-1);
}
if (j < high) {
quickSortAscRe(arr, i+1, high);
}
}
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。
堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。
堆排序动图演示:
/**
* 堆排序
* asc
* 升序
* @param arr
*/
public static void heapSortAsc(int[] arr){
int len = arr.length;
if (len <= 1)
return;
//建堆
int startIndex = getParentIndex(len-1);//从最后一个节点的父节点开始创建堆
for (int i = startIndex; i >=0 ; i--) {
buildHeapAsc(arr,len,i);
}
//头与尾交换,交换之后再建堆。之后再交换
for (int i = len-1; i >0 ; i--) {
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
//交换之后原堆除了跟节点,其他的子节点仍是大根堆。所以只要重新调整index为0的堆。堆的大小刚好为i
buildHeapAsc(arr, i, 0);
}
}
/**
*
* @param arr 堆数组
* @param size 堆的大小
* @param i 保证父节点i为大根堆
*/
private static void buildHeapAsc(int[] arr, int size, int i) {
int left = getLeftChildIndex(i);
int right = getRightChildIndex(i);
int max = i;
if(left < size && arr[left] > arr[max]){
max = left;
}
if(right < size && arr[right] > arr[max]){
max = right;
}
if(max != i){
//交换
int temp = arr[i];
arr[i] = arr[max];
arr[max] = temp;
//交换之后的子节点可能不是最大堆。需要重新建堆
buildHeapAsc(arr, size, max);
}
}
/**
* 返回左子节点
*/
private static int getLeftChildIndex(int i){
return 2*i + 1;
}
/**
* 返回右子节点
*/
private static int getRightChildIndex(int i){
return 2*i + 2;
}
/**
* 返回父节点
*/
private static int getParentIndex(int i){
return (i-1)/2;
}
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序)
计数排序的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。
/**
* 计数排序非负数
* asc
* 升序
* 比较适合于数量大但数值的范围是在一定限度内。
* 比如统计100万学生高考成绩的排名,由于高考成绩一定是在0-满分(假如750)之间。
* 所以一次遍历便可以统计每个分数所对应的人数。(假设分数是整数)
* 最后根据每个分数对应的人数排序即可。
* @param arr
*/
public static void countingSortAsc(int[] arr){
//先确定最大值和最小值
int max = arr[0];
int min = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
if (min > arr[i]) {
min = arr[i];
}
}
//定义一个有序集
int[] order = new int[max-min+1];//arr数组中所有的数一定是在区间[min,max]
//统计区间中的每个数有多少个。为了不浪费空间,我们将数组arr中的数都减去min,得到的值对应order的下边。
//则order[index] = 数组arr中值为index+min的数量
for (int i = 0; i < arr.length; i++) {
order[arr[i]-min] += 1;
}
int k = 0;
for (int i = 0; i < order.length; i++) {
for (int j = 0; j < order[i]; j++) {
arr[k++] = i+min;
}
}
}
将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
桶排序的具体实现算法可能有很多种,这里提供的例子只使用了桶排序。具体算法说明如下:
桶排序动图演示:
/**
* 桶排序
* asc
* 升序
* @param arr
*/
public static void BucketSortAsc(int[] arr){
int n = arr.length;
int bucket[][] = new int[10][n];
int index[] = new int[10];
int intLen = 0;//数组中位数最多的数
int min = 0;//主要用于处理负数的情况
for (int i = 0; i < arr.length; i++) {
if(arr[i] < min){
min = arr[i];
}
}
for (int i = 0; i < arr.length; i++) {
arr[i] -= min;//变为正数处理
if ((""+arr[i]).length() > intLen) {
intLen = (""+arr[i]).length();
}
}
for (int i = 0; i < intLen; i++) {//从个位数开始比较。如果该位数为0,则放在第一个桶子里,以此类推。
for (int j = 0; j < arr.length; j++) {
int temp = 0;
String str = Integer.toString(arr[j]);
if(str.length()>i){
temp = str.charAt(str.length()-i-1)-'0';
}
//temp表示当前位数i对应的数字。
bucket[temp][index[temp]++] = arr[j];
}
//经过一次分类之后。每个桶按照当前位数从小到大排列了。即第1一个桶子的位数i均为0。
//等到排列下一位的时候,该位相同的数会被放入同一个桶子里,而该位的低位已经在前一次循环中排好序了。
//所以对每一位桶排序之后,整个数组就有序了。
int pos = 0;
for (int j = 0; j < index.length; j++) {
for (int k = 0; k < index[j]; k++) {
arr[pos++] = bucket[j][k];
}
}
for (int j = 0; j < index.length; j++) {
index[j] = 0;
}
}
for (int i = 0; i < arr.length; i++) {
arr[i] += min;
}
}
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)。
回顾之前的桶排序和计数排序,其实原理类似。基数排序分为最高位优先(Most Significant Digit first)法,简称MSD法,和最低位优先(Least Significant Digit first)法,简称LSD法。本文所讲的桶排序就是LSD法。
基数排序LSD动图演示:
最高位优先(Most Significant Digit first)法,简称MSD法:MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。
/**
* 基数排序MSD
* asc
* 升序
* @param arr
*/
public static void RadixSortAsc(int[] arr){
int min = 0;//主要用于处理负数的情况
for (int i = 0; i < arr.length; i++) {
if(arr[i] < min){
min = arr[i];
}
}
int max = 0;
for (int i = 0; i < arr.length; i++) {//全部转化为非负数,并获得最大位数
arr[i] -= min;//变为正数处理
if(arr[i] > max){
max = arr[i];
}
}
int maxLen = Integer.toString(max).length();//数组中最大数的位数
/**
* 先对最高位进行桶排序
*/
RadixSortAsc(arr,arr.length,maxLen-1);//先对最高位排序
if (min<0) {
for (int i = 0; i < arr.length; i++) {
arr[i] += min;
}
}
}
/**
* 递归
* @param arr
* @param n 待桶排序的数据
* @param i 待排序的位数
*/
private static void RadixSortAsc(int[] arr,int n, int i) {
int bucket[][] = new int[10][n];
int index[] = new int[10];
for (int j = 0; j < n; j++) {
int temp = 0;
String str = Integer.toString(arr[j]);
if(str.length()>i){
temp = str.charAt(str.length()-i-1)-'0';
}
//temp表示当前位数i对应的数字。
bucket[temp][index[temp]++] = arr[j];
}
//对每个桶里面的数据再进行桶排序
for (int j = 0; j < index.length; j++) {
if(i<=0)
break;
if(index[j] == 0)
continue;
RadixSortAsc(bucket[j],index[j], i-1);//依次对每个桶的数据进行桶排序
}
int pos = 0;
for (int j = 0; j < index.length; j++) {
for (int k = 0; k < index[j]; k++) {
arr[pos++] = bucket[j][k];
}
}
}
十大经典排序算法总结(JavaScript描述)
时间复杂度和空间复杂度详解