排序算法总结以及其Java实现

时间:2023-02-15 18:14:49

  近日一直在准备着即将到来的实习,对以前的基础知识开始进行再次巩固。其中,最重要的就是几种常见的排序算法了。

  所谓排序,即将原来无序的一个序列重新排列成有序的序列。

  排序方法中涉及到稳定性,所谓稳定性,是指待排序的序列中有两个或两个以上相同的项在排序前和排序后看
这些相同项的相对位置有没有发生变化
,如果没有发生变化,即该排序方法是稳定的,如果发生变化,则说明该
排序方法是不稳定的。

  那么哪些算法是不稳定的呢?快速排序、希尔排序、选择排序、堆排序;即所谓的“快些选堆”,而其他的都是稳定的。

排序算法分类

1、插入类排序
即在一个已经有序的序列中,插入一个新的记录,就好比军训排队,已经排好一个纵队,这时来了个新家伙,于
是新来的“插入”这个队伍中的合适位置。这类排序有:直接插入排序、折半插入排序、希尔排序。

2、交换类排序
该类方法的核心是“交换”,即每趟排序,都是通过一系列的“交换”动作完成的,如军训排队时,教官说:你比旁
边的高,你俩交换下,还比下一个高就继续交换。这类排序有:冒泡排序、快速排序。
3、选择类排序
该方法的核心是“选择”,即每趟排序都选出一个最小(或最大)的记录,把它和序列中的第一个(或最后一个)
记录交换,这样最小(或最大)的记录到位。如军训排队时,教官选出个子最小的同学,让他和第一个位置的同
学交换,剩下的继续选择。这类排序有:选择排序、堆排序。
4、归并类排序
所谓归并,就是将两个或两个以上的有序序列合并成一个新的有序序列。如军训排队时,教官说:每个人先和旁
边的人组成二人组,组内排好队,二人组和旁边的二人组组成四人组,内部再排好队,以此类推,直到最后全部
同学都归并到一个组中并排好序。这类排序有:(二路)归并排序。
5、基数类排序
此类方法较为特别,是基于多关键字排序的思想,把一个逻辑关键字拆分成多个关键字,如一副扑克牌,按照基
数排序思想可以先按花色排序,则分成4堆,每堆再按A-K的顺序排序,使得整副扑克牌最终有序。

排序算法具体分析

准备工作:

新建一个类,作为一个测试算法的类,因为算法之中经常用到交换算法,故而在里面封装一个工具类Swap类,该类如下:
package 排序算法;

/**
* 对排序算法进行测试
*/
public class SortTest {

/**
* 指定数组内两个元素进行交换
*
* @param nums 表示传入的数组
* @param index1 表示要交换的第一个元素的下标
* @param index2 表示要交换的第二个元素的下标
*/
public static void Swap(int[] nums, int index1, int index2) {
// TODO Auto-generated method stub
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}

public static void main(String[] args) {

int[] nums = {23, 44, 11, 344, 55566, 778, 90, 22, 88};
//此处输入要进行测试的算法。
for (int i : nums) {
System.out.println(i);
}
}


}

1.插入排序:

算法思想:每趟将一个待排序的关键字,按照其关键字值的大小插入到已经排好的部分序列的适当位置上,直到
插入完成。

算法实现:
package 排序算法;

/**
* 插入排序:每次将一个待排序的关键字,按照关键字的值的大小,插入已排好序的部分序列的位置上面
* 直到插入完成
* 时间复杂度最好情况是O(n),最差是O(n*n)
*/
public class InsertSort {
/**
* 插入排序:每次将一个待排序的关键字,按照关键字的值的大小,插入已排好序的部分序列的位置上面
* 直到插入完成
* 时间复杂度最好情况是O(n),最差是O(n*n)
*/
public static void InsertionSort(int[] nums) {
for (int i = 0; i < nums.length; i++) {
int j = i, value = nums[i];
while (j > 0 && nums[j - 1] > value) {
nums[j] = nums[j - 1];
j--;
}
nums[j] = value;
}
}
}

性能分析:
在内层循环中this[j]=this[j-1],这句是作为基本操作。考虑最坏情况,即整个序列是逆序的,则其基
本操作总的执行次数为n*(n-1)/2,其时间复杂度为O(n*n)。考虑最好情况,即整个序列已经有序,则循环内的操
作均为常量级,其时间复杂度为O(n)。因此本算法平均时间复杂度为O(n*n)。算法所需的额外空间只有一
个value,因此空间复杂度为O(1)。


2.希尔排序:

算法思想:
  希尔排序又叫做缩小增量排序,是将待排序的序列按某种规则分成几个子序列,分别对这几个子序列
进行插入排序,其中这一规则就是增量。如可以使用增量5、3、1来分格序列,且每一趟希尔排序的增量都是逐
渐缩小的,希尔排序的每趟排序都会使得整个序列变得更加有序,等整个序列基本有序了,再使用一趟插入排
序,这样会更有效率,这就是希尔排序的思想。

算法实现:
package 排序算法;

/**
* 希尔排序:又叫缩小增量排序,又叫做缩小增量排序,
* 是将待排序的序列按某种规则分成几个子序列,分别
* 对这几个子序列进行插入排序,其中这一规则就是增量
* 如可以使用增量5、3、1来分格序列,且每一趟希尔排序
* 的增量都是逐渐缩小的,希尔排序的每趟排序都会使得
* 整个序列变得更加有序,等整个序列基本有序了,再
* 使用一趟插入排序,这样会更有效率,这就是希尔排序的思想
*/
public class ShellSort {
/**
* 算法性能:希尔排序的时间复杂度平均情况为O(nlogn),
* 空间复杂度为O(1)。希尔排序的增量取法要注意,首先
* 增量序列的最后一个值一定是1,其次增量序列中的值
* 没有除1之外的公因子,如8,4,2,1这样的序列就
* 不要取(有公因子2)。
*/
public static void shellSort(int[] nums) {
for (int step = nums.length >> 1; step > 0; step >>= 1) {
System.out.println("增量是:" + step);
for (int i = 0; i < step; i++) {
for (int j = i + step; j < nums.length; j += step) {
int k = j, value = nums[j];
while (k > step && nums[k - step] > value) {
nums[k] = nums[k - step];
k -= step;
}
nums[k] = value;
}
}
}
}
}


性能分析:
希尔排序的时间复杂度平均情况为O(nlogn),空间复杂度为O(1)。希尔排序的增量取法要注意,首先
增量序列的最后一个值一定是1,其次增量序列中的值没有除1之外的公因子,如8,4,2,1这样的序列就不要取
(有公因子2)。

3.冒泡排序

算法思想:
    通过一系列的“交换”动作完成的,首先第一个记录与第二个记录比较,如果第一个大,则二者交换, 否则不交换;然后第二个记录和第三个记录比较,如果第二个大,则二者交换,否则不交换,以此类推,最终最 大的那个记录被交换到了最后,一趟冒泡排序完成。在这个过程中,大的记录就像一块石头一样沉底,小的记录 逐渐向上浮动。冒泡排序算法结束的条件是一趟排序没有发生元素交换。

算法实现:
package 排序算法;
/**
* 冒泡排序:通过一系列的“交换”动作完成的,首先第一个记录与第二个记录比较,
* 如果第一个大,则二者交换,否则不交换;然后第二个记录和第三个
* 记录比较,如果第二个大,则二者交换,否则不交换,以此类推,
* 最终最大的那个记录被交换到了最后,一趟冒泡排序完成。
* 在这个过程中,大的记录就像一块石头一样沉底,小的记录逐渐向上浮动。
* 冒泡排序算法结束的条件是一趟排序没有发生元素交换。
* */
public class BubbleSort {
/**
* 最内层循环的元素交换操作是算法的基本操作。最坏情况,待排序列逆序,
* 则基本操作的总执行次数为(n-1+1)*(n-1)/2=n(n-1)/2,其时间
* 复杂度为O(n*n);最好情况,待排序列有序,则时间复杂度为O(n),
* 因此平均情况下的时间复杂度为O(n*n)。算法的额外辅助空间只有一个
* 用于交换的temp,所以空间复杂度为O(1)
* */
public static void bubbleSort(int[] nums){
for(int i=nums.length-1;i>0;i--){
for(int j=0;j<i;j++){
if(nums[j]>nums[j+1]){
SortTest.Swap(nums,j, j+1);
}
}
}
}

}

性能分析:
最内层循环的元素交换操作是算法的基本操作。最坏情况,待排序列逆序,则基本操作的总执行次数
为(n-1+1)*(n-1)/2=n(n-1)/2,其时间复杂度为O(n*n);最好情况,待排序列有序,则时间复杂度为O(n),因此平
均情况下的时间复杂度为O(n*n)。算法的额外辅助空间只有一个用于交换的temp,所以空间复杂度为O(1)。

4.快速排序

算法思想:
以军训排队为例,教官说以第一个同学为中心,比他矮的站他左边,比他高的站他右边,这就是一趟快速排序。因此,一趟快速排序是以一个枢轴,将序列分成两部分,枢轴的一边比它小(或小于等于),另一边比它大(或大于等于)。

算法实现:
package 排序算法;


/**
* 快速排序:以军训排队为例,教官说以第一个同学为中心,比他矮的站他左边,
* 比他高的站他右边,这就是一趟快速排序。因此,一趟快速排序是
* 以一个枢轴,将序列分成两部分,枢轴的一边比它小(或小于等于),
* 另一边比它大(或大于等于)。
*/
public class QuickSort {
/**
* 快速排序最好情况下时间复杂度为O(nlogn),待排序列越接近无序,
* 则该算法效率越高,在最坏情况下时间复杂度为O(n*n),待排序列
* 越接近有序,则该算法效率越低,算法的平均时间复杂度为O(nlogn)。
* 就平均时间而言,快速排序是所有排序算法中最好的。该算法的空间
* 复杂度为O(logn),快速排序是递归进行的,需要栈的辅助,因此
* 需要的辅助空间比前面几类排序方法要多
*/
public static void quickSort(int[] nums, final int low, final int high) {
int start = low;
int end = high;
int key = nums[low];

while (end > start) {
//从后往前比较
while (end > start && nums[end] >= key) {
//如果没有比关键值小的,比较下一个,直到有比关键值小的交换位置,然后又从前往后比较
end--;
}

if (nums[end] <= key) {
SortTest.Swap(nums, start, end);
}

//从前往后比较
while (end > start && nums[start] <= key)//如果没有比关键值大的,比较下一个,直到有比关键值大的交换位置
start++;

if (nums[start] >= key) {
SortTest.Swap(nums, start, end);
}

}
//递归
if (start > low) quickSort(nums, low, start - 1);//左边序列,第一个索引值位置到关键值位置索引-1
if (end < high) quickSort(nums, end + 1, high);//右边序列,关键值位置索引+1到最后一个索引值
}

}

性能分析:

快速排序最好情况下时间复杂度为O(nlogn),待排序列越接近无序,则该算法效率越高,在最坏情况
下时间复杂度为O(n*n),待排序列越接近有序,则该算法效率越低,算法的平均时间复杂度为O(nlogn)。就平均
时间而言,快速排序是所有排序算法中最好的。该算法的空间复杂度为O(logn),快速排序是递归进行的,需要
栈的辅助,因此需要的辅助空间比前面几类排序方法要多。

注:
快速排序的效率和选取的“枢轴”有关,选取的枢轴越接近中间值,算法效率就越高,因此为了提高算
法效率,可以在第一次选取“枢轴”时做文章,如在数据堆中随机选取3个值,取3个值的平均值作
为“枢轴”,就如抽样一般。关于具体如何提高快速排序算法的效率,在本文不做详细介绍了,点到为
止。(感兴趣的读者可以自行去研究)

5.选择排序

算法思想:
该算法的主要动作就是“选择”,采用简单的选择方式,从头至尾顺序扫描序列,找出最小的一个记
录,和第一个记录交换,接着从剩下的记录中继续这种选择和交换,最终使序列有序。

算法实现:
package 排序算法;
/**
* 选择排序:该算法的主要动作就是“选择”,采用简单的选择方式,从头至尾顺序扫描序列,
* 找出最小的一个记录,和第一个记录交换,接着从剩下的记录中继续这种选择和
* 交换,最终使序列有序
* */
public class SelectionSort {
/**
* 将最内层循环中的比较视为基本操作,其执行次数为
* (n-1+1)*(n-1)/2=n(n-1)/2,其时间复杂度为
* O(n*n),本算法的额外空间只有一个temp,因此
* 空间复杂度为O(1)。
* */
public static void selectionSort(int[] nums){
for(int i=0;i<nums.length;i++){
int index=i;
for(int j=i+1;j<nums.length;j++){
if(nums[j]<nums[index]){
index=j;
}
}
SortTest.Swap(nums,i, index);
}
}


}

性能分析:
:将最内层循环中的比较视为基本操作,其执行次数为(n-1+1)*(n-1)/2=n(n-1)/2,其时间复杂度
为O(n*n),本算法的额外空间只有一个temp,因此空间复杂度为O(1)。

6.堆排序

算法思想:
堆是一种数据结构,最好的理解堆的方式就是把堆看成一棵完全二叉树,这个完全二叉树满足任何一
个非叶节点的值,都不大于(或不小于)其左右孩子节点的值。若父亲大孩子小,则这样的堆叫做大顶堆;若父
亲小孩子大,这样的堆叫做小顶堆。根据堆的定义,其根节点的值是最大(或最小),因此将一个无序序列调整
为一个堆,就可以找出这个序列的最大(或最小)值,然后将找出的这个值交换到序列的最后(或最前),这样
有序序列元素增加1个,无序序列中元素减少1个,对新的无序序列重复这样的操作,就实现了序列排序。堆排序
中最关键的操作是将序列调整为堆,整个排序的过程就是通过不断调整使得不符合堆定义的完全二叉树变为符合
堆定义的完全二叉树的过程。

算法实现:
package 排序算法;
/**
* 堆排序:将一个无序序列调整为一个堆,就可以找出这个序列的
* 最大(或最小)值,然后将找出的这个值交换到序列的最后(或
* 最前),这样有序序列元素增加1个,无序序列中元素减少1个,
* 对新的无序序列重复这样的操作,就实现了序列排序。
*
* 堆排序中最关键的操作是将序列调整为堆,整个排序的过程就是
* 通过不断调整使得不符合堆定义的完全二叉树变为符合堆定义的
* 完全二叉树的过程
* */
public class HeapSort {
/**
* 完全二叉树的高度为[log(n+1)],即对每个节点调整的时间
* 复杂度为O(logn),基本操作总次数是两个并列循环中基本
* 操作次数相加,则整个算法时间复杂度为O(logn)*n/2+O(logn)*(n-1),
* 即O(nlogn)。额外空间只有一个temp,因此空间复杂度为O(1)。
* */
public static void heapSort(int[] nums){
if(nums == null||nums.length <= 1){
return;
}
BuildMaxHeap(nums);

for (int i = nums.length - 1; i >= 1; i--) {
SortTest.Swap(nums, 0, i);

maxHeap(nums, i, 0);
}
}
/**
* 构建最大堆
* */
private static void BuildMaxHeap(int[] array){
if (array == null || array.length <= 1) {
return;
}
int half = array.length/2;
for(int i=half;i>=0;i--){
maxHeap(array, array.length, i);
}
}
/**
* 该函数假设一个元素的两个子节点都满足最大堆的性质(左右子树都是最大堆),
* 只有根元素可能违反最大堆性质,那么把该元素以及左右子节点的最大元素找出来,
* 如果该元素已经最大,那么整棵树都是最大堆,程序退出,否则交换跟元素与最大
* 元素的位置,继续调用maxHeap原最大元素所在的子树。该算法是分治法的典型应用。
* */
private static void maxHeap(int[] array,int heapSize,int index){
int left = index * 2 + 1;
int right = index * 2 + 2;

int largest = index;
if (left < heapSize && array[left] > array[index]) {
largest = left;
}

if (right < heapSize && array[right] > array[largest]) {
largest = right;
}

if (index != largest) {
SortTest.Swap(array, index, largest);

maxHeap(array, heapSize, largest);
}
}
}

性能分析:
完全二叉树的高度为[log(n+1)],即对每个节点调整的时间复杂度为O(logn),基本操作总次数是两个
并列循环中基本操作次数相加,则整个算法时间复杂度为O(logn)*n/2+O(logn)*(n-1),即O(nlogn)。额外空间只
有一个temp,因此空间复杂度为O(1)。

7.归并排序

算法思想:
其核心就是“两两归并”,首先将原始序列看成每个只含有单独1个元素的子序列,两两归并,形成若干有序二元组,则第一趟归并排序结束,再将这个序列看成若干个二元组子序列,继续两两归并,形成若干有序四元组,则第二趟归并排序结束,以此类推,最后只有两个子序列,再进行一次归并,即完成整个归并排序。

算法实现:
package 排序算法;
/**
* 归并排序:其核心就是“两两归并”,首先将原始序列看成每个只含有
* 单独1个元素的子序列,两两归并,形成若干有序二元组,则第一趟
* 归并排序结束,再将这个序列看成若干个二元组子序列,继续两两归并,
* 形成若干有序四元组,则第二趟归并排序结束,以此类推,最后只有
* 两个子序列,再进行一次归并,即完成整个归并排序。
* */
public class MergeSort {
/**
* 将两个数组进行归并,归并前面2个数组已有序,归并后依然有序
*
* @param nums
* 数组对象
* @param low
* 左数组的第一个元素的索引
* @param mid
* 左数组的最后一个元素的索引,mid+1是右数组第一个元素的索引
* @param high
* 右数组最后一个元素的索引
* */
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)
* 稳定排序方式
* @param nums 待排序数组
* @return 输出有序数组
*/
public static int[] mergeSort(int[] nums, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
// 左边
mergeSort(nums, low, mid);
// 右边
mergeSort(nums, mid + 1, high);
// 左右归并
merge(nums, low, mid, high);
}
return nums;
}

}


性能分析:
可以选取“归并操作”作为基本操作,“归并操作”即为将待归并表中元素复制到一个存储归并结果的表
中的过程,其次数为要归并的两个子序列中元素个数之和。算法总共需要进行logn趟排序,每趟排序执行n次基
本操作,因此整个归并排序中总的基本操作执行次数为nlogn,即时间复杂度为O(nlogn),说明归并排序时间复
杂度和初始序列无关。由于归并排序需要转存整个待排序列,因此空间复杂度为O(n)。


最终总结:

(1)快速排序、希尔排序、归并排序、堆排序的平均时间为O(nlogn),其他的为O(n*n)。
(2)快速排序、希尔排序、选择排序、堆排序不稳定,其他的稳定
(3)经过一趟排序能够保证一个元素到达最终位置的是冒泡排序、快速排序、选择排序、堆排序。
(4)元素比较次数和原始序列无关的是选择排序、折半插入排序。
(5)排序趟数和原始序列有关的是交换类排序。
(6)直接插入排序和折半插入排序的区别是寻找插入位置的方式不同,一个是按顺序查找方式,另一个是按折
半查找方式。