简介
什么是排序的稳定性?
所谓稳定性指的是当待排序序列中有两个或两个以上的相同关键字时,排序前和排序后这些关键字的相对位置,如果没有发生变化就是稳定,否则就是不稳定的。
关于排序具体的介绍接下来一个一个开始:
直接插入排序:
代码如下:
package com.wzc;/*
*@date 2021/2/23
* @author wzc
*/
import java.util.Arrays;
public class InsertionSort {
public static void main(String[] args) {
int[] arr = new int[]{49,38,65,97,76,13,27,49};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
//如果当前数字比前一个数字小
if (arr[i] < arr[i-1]){
//先把当前数字存起来
int temp = arr[i];
int j;
for ( j = i-1; j>= 0 && temp < arr[j]; j--) {
arr[j+1] = arr[j];
}
arr[j+1] = temp;
}
}
}
}
编译结果:
算法所需要的辅助存储空间不随待排序列规模的变化而变化,是一个常量所以空间复杂度为0(1)。
时间复杂度:由于是双层for循环n所以时间复杂度为0(n^2)。.
稳定性判定:
倒数第二次排序顺序:13 27 38 49 65 76 97 49
for循环内部判定条件是满足temp < arr[j],所以当后一个49和前一个49比较,不满足小于号这个条件,j指向的是前一个49,所以直接执行语句 arr[j+1] = temp;,temp赋值给arr[4],前一个49和后一个49位置没有交换,所以稳定。
最后的排序顺序:13 27 38 49 49 65 76 97
希尔排序
package com.wzc;/*
*@date 2021/2/23
* @author wzc
*/
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args) {
int[] arr = {49,38,65,97,76,13,27,49,55,4};
System.out.println(Arrays.toString(arr));
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void shellSort(int[] arr) {
//遍历所有步长
int k = 1;
for (int d = arr.length/2; d>0 ; d/=2) {
for (int i = d; i < arr.length; i++) {
for (int j = i-d; j >=0 ; j-=d) {
if (arr[j] > arr[j+d]){
int temp = arr[j];
arr[j] = arr[j+d];
arr[j+d] = temp;
}
}
}
System.out.println("第"+k+"次排序结果:"+Arrays.toString(arr));
k++;
}
}
}
时间复杂度分析较复杂,暂时略过。
空间复杂度同理,只用了temp临时变量,所以空间复杂度为0(1)。
希尔排序不稳定,第一趟希尔排序后,两个49就颠倒了,如下图所示:
冒泡排序
package com.wzc;/*
*@date 2021/2/23
* @author wzc
*/
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {49,38,65,97,76,13,27,49};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void bubbleSort(int[] arr) {
int k =1;
for (int i = 0; i < arr.length-1; i++) {
for (int j = 0; j < arr.length-i-1; j++) {
if (arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
System.out.println("第"+k+"次排序结果:"+Arrays.toString(arr));
k++;
}
}
}
编译结果:
该代码需要优化,因为排序完成后仍旧重复排序。
平均时间复杂度双层n次for循环,时间复杂度为0(n^2)。
空间复杂度同理,额外辅助空间只有一个temp,空间复杂度为0(1)。
并且可以发现过程中2个相同关键字49没有发生关键字的交换,原因在于代码中只有前者比后者大才交换位置,等于不交换,所以冒泡排序稳定。
package com.wzc;/*
*@date 2021/2/23
* @author wzc
*/
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {49,38,65,97,76,13,27,49};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void bubbleSort(int[] arr) {
int k =1;
for (int i = 0; i < arr.length-1; i++) {
int flag = 1;
for (int j = 0; j < arr.length-i-1; j++) {
if (arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = 0;
}
}
if (flag == 1){
return;
}
System.out.println("第"+k+"次排序结果:"+Arrays.toString(arr));
k++;
}
}
}
优化只需要加flag标签即可,编译结果:
快速排序
package com.wzc;/*
*@date 2021/2/23
* @author wzc
*/
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int[] arr = {49,38,65,97,76,13,27,49};
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr,int start,int end) {
//递归结束条件
if (start < end){
//设置标准数
int s = arr[start];
int low = start;
int high = end;
while (low<high){
while (low<high&&s<=arr[high]){
high--;
}
arr[low] = arr[high];
while (low<high&&s>=arr[low]){
low++;
}
arr[high] = arr[low];
}
arr[low] = s;
quickSort(arr, start, low);
quickSort(arr, low+1,end);
}
}
}
编译结果:
快速排序中对每一个子序列的一次划分算作一趟排序,每一趟排序结束后有一个关键字到达最终位置。
快速排序最好情况下时间复杂度为0(nlog2 n),待排序列越接近无序,本算法效率越高,越接近有序,效率越低,最坏情况时间复杂度为0(n^2),平均时间复杂度为0(nlog2 n)。
快速排序是递归进行的,递归需要栈的辅助,因此它需要的辅助空间比前面几类排序算法大,空间复杂度为0(nlog2 n)。
不稳定性体现在:假设一个数组int[] a = {1, 3, 3, 4, 5, 2};
3,3是相等的,但是在排序过程中可能会发生位置调换,由图可见:
第一个3被安排到了最后,第二个3在它的前面,相同关键字顺序交换,所以不稳定。
简单选择排序
package com.wzc;/*
*@date 2021/2/24
* @author wzc
*/
import java.lang.reflect.Array;
import java.util.Arrays;
public class SelectSort {
public static void main(String[] args) {
int[] arr = {49,38,65,97,76,13,27,49};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void selectSort(int[] arr) {
int k = 0;
for (int i = 0; i < arr.length-1; i++) {
int minindex = i;
//当前遍历的数和后面所有的数依次比较,并记录最小的数的下标
for (int j = i+1; j < arr.length; j++) {
if (arr[j] < arr[minindex]){
minindex = j;
}
}
//如果最小的数的下标和当前遍历数的下标不一致,说明下标为minindex的数为最小值
if (minindex != i){
int temp = arr[i];
arr[i] = arr[minindex];
arr[minindex] = temp;
}
k++;
System.out.println("第"+k+"次排序结果:"+Arrays.toString(arr));
}
}
}
空间复杂度同理,算法所需的辅助存储空间不随待排序列规模的变化而变化,是个常量,因此空间复杂度为0(1)。
时间复杂度也是双层n次for循环,为0(n^2).。
简单选择排序是不稳定的。例子如下图:
有一个数组顺序是4 3 4 1 5
二路归并排序
package com.wzc;/*
*@date 2021/2/24
* @author wzc
*/
import java.util.Arrays;
public class MergeSort {
public static void main(String[] args) {
int[] arr = {49,38,65,97,76,13,27};
System.out.println(Arrays.toString(arr));
mergeSort(arr,0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
//归并排序
public static void mergeSort(int[] arr,int low,int high){
int middle = (low+high)/2;
if (low < high){
//处理左边
mergeSort(arr,low,middle);
//处理右边
mergeSort(arr,middle+1,high);
//进行归并
merge(arr,low,middle,high);
}
}
public static void merge(int[] arr,int low,int middle,int high) {
//创建一个临时数组
int[] temp = new int[high-low+1];
//记录第一个数组中需要遍历的下标
int i = low;
//记录第二个数组中需要遍历的下标
int j = middle+1;
//记录临时数组需要遍历的下标
int index = 0;
while (i<=middle&&j<=high){
if (arr[i] <= arr[j]){
temp[index] = arr[i];
i++;
}else {
temp[index] = arr[j];
j++;
}
index++;
}
//处理多余数据
while (i<=middle){
temp[index] = arr[i];
i++;
index++;
}
//处理多余数据
while (j<=high){
temp[index] = arr[j];
j++;
index++;
}
//把临时数组的数据重新存入原数组
for (int k = 0; k < temp.length; k++) {
arr[k+low] = temp[k];
}
}
}
过程:
- 将原始序列看成7个只含有一个关键字的子序列,显然这些子序列都是有序的。
- 然后两两归并,形成若干个有序二元组 {38,49} {65,79} {13,76}{27}
- 接着两两归并,形成若干四元组{38,49,65,97}{13,27,76}
- 再归并,完成整个排序。
时间复杂度为0(nlog2 n),log2 n趟,每趟执行n次。
因归并排序需要转存整个待排序列,因此空间复杂度为0(n)
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素或者2个序列,然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。
可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等,没有外部干扰,将不会破坏稳定性,所以它稳定。
关于二路归并中的递归看下图例子:
基数排序
一共有十一个乱序数
按照每个数的个位数字依次放在相对应数字的桶中
先放进去先取,依次取出来
再按照十位数字依次排进去
再依次取出来
第三次按百位排,依次按照这样规律排序
。。。
最终排序结果:
需要排几次,取决于这串数中的最高位数是多少。
由上述的示意图可知进出桶的顺序是先进先出,所以可以采用队列。
其代码:
package com.wzc;/*
*@date 2021/2/25
* @author wzc
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
public class RadixQuequeSort {
public static void main(String[] args) {
int[] a = {278, 109, 63, 930, 589, 184, 505, 269, 8, 83};
radixQuequeSort(a);
System.out.println(Arrays.toString(a));
}
public static void radixQuequeSort(int[] a) {
//首先确定排序的趟数;
int max = a[0];
//找到最大值
for (int i = 1; i < a.length; i++) {
if (a[i] > max) {
max = a[i];
}
}
//令time等于位数
int time = 0;
//判断位数;
while (max > 0) {
max /= 10;
time++;
}
//建立10个队列;
List<ArrayList<Integer>> queue = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++) {
ArrayList<Integer> queue1 = new ArrayList<Integer>();
queue.add(queue1);
}
//进行time次分配和收集;
for (int i = 0; i < time; i++) {
//分配数组元素;
for (int j = 0; j < a.length; j++) {
//得到数字的第time+1位数;
//pow(x,y) 方法可返回 x 的 y 次幂的值。
int x = a[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
ArrayList<Integer> queue2 = queue.get(x);
queue2.add(a[j]);
queue.set(x, queue2);
}
int count = 0;
//元素计数器;
//收集队列元素;
for (int k = 0; k < 10; k++) {
while (queue.get(k).size() > 0) {
ArrayList<Integer> queue3 = queue.get(k);
a[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
}
基础排序稳定,相同关键字遵循先进先出,相同关键字前后顺序相同。
堆排序
堆排序具体过程如图
package com.wzc;/*
*@date 2021/3/3
* @author wzc
*/
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] arr = {9, 6, 8, 7, 0, 1, 10, 4, 2};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[] arr) {
//开始位置是最后一个非叶结点,即最后一个节点的父结点
int start = (arr.length - 1) / 2;
//创建初始堆
for (int i = start; i >= 0; i--) {
maxHeap(arr, arr.length, i);
}
//交换后筛选调整
for (int i = arr.length - 1; i > 0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
maxHeap(arr, i, 0);
}
}
//形参size是为了防止数组越界
public static void maxHeap(int[] arr, int size, int index) {
int leftNode = 2 * index + 1;
int rightNode = 2 * index + 2;
int max = index;
if (leftNode < size && arr[leftNode] > arr[max]) {
max = leftNode;
}
if (rightNode < size && arr[rightNode] > arr[max]) {
max = rightNode;
}
if (max != index) {
int temp = arr[index];
arr[index] = arr[max];
arr[max] = temp;
//交换位置后可能会影响之前排好的堆,之前的排好的堆需要重新调整
maxHeap(arr, size, max);
}
}
}
编译结果:
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)
空间复杂度因为是就地排序所以为O(1)
堆排序不稳定,因为在堆的调整过程中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径,因此对于相同的关键字就可能出现排在后面的关键字被交换到前面来的情况。
外部排序
所谓外部排序就是对外存中的记录进行排序(相对于内部排序而言),由于外部排序中记录的规模太大,导致内存放不下,外部排序可以概括为一句话;将内存作为工作空间来辅助外部排序。
外部排序最常用的算法是归并排序。
有一个待排序列,由图可见,内存无法装下外存中的全部关键字
将整个序列依次放入内存中,依次归并成三个一组的有序序列(三个初始归并段)
将三个序列的最值关键字全部放入内存中,然后再在内存中挑选出最值关键字,并且由它所在序列中的后一个关键字填补空位
按照以上操作重复继续进行,最后在外存上得到有序序列,这就是多路归并排序外部排序。
虽然完成了排序,但是由于io操作,实现时可能会很慢,当前排序每个关键字执行了四次io操作,因此如何减少io操作成为外部排序的一个问题。
置换选择排序:一种适用于初始归并规模的,高效的且不受内存空间限制的排序算法。可以使初始归并段长度大于内存长度
将一个序列前四个全部放入内存中
读出内存中最小的值,空位被序列中下一个关键字占
重复上述操作直到有序序列最后一个关键字,比内存中最小的关键字大,标记
标记后,再重复以上操作,直到卡到如图位置,将已经排好的序列作为第一个初始归并段放在一边
然后将标记去除
然后重复之前排序操作,最后如图,此时得到第二个有序序列
最后得到以下三个长度不等有序序列段
最佳归并树
以下是通过置换选择排序得到的9个长度不等的序列,用圆圈分别代替,圆圈中的数字代表归并段的长度
下图为最佳归并树,如何得到?
该图进行三路归并,选择关键字中个数最少的三个归并,如2,3,6,归并成一个11的有序序列,再找出个数最少的三个序列,反复操作以上过程,最终得到最佳归并树。
最佳归并树减少了io操作,每一条分支相当于两次io操作,最佳归并树最小化io的次数
下图是普通的归并树与其io次数计算
排序知识点小结
平均情况下时间复杂度,快速排序,归并排序,堆排序,希尔排序(了解即可)的实践复杂度均为O(nlog2 n),除基数排序其他全是O(n^2).(快些归队)
最坏情况下快速排序的时间复杂度都是O(n^2)
空间复杂度记住几个特殊的就好,快速排序O(log2 n),归并排序O(n),基数排序O(r),其余都是O(1)
直接插,起得好:直接插入排序容易插成O(n),起泡排序起得好容易变成O(n)
算法稳定性:
快些选一堆:快速排序,希尔排序,简单选择排序,堆排序不稳定,其余排序都稳定。
经过一趟排序,能保证一个关键字;到达最终位置,这样的排序是交换类的两种(起泡和快速),和选择类的两种(堆排序和简单选择)
排序算法比较次数和原式序列无关:简单选择排序,折半插入排序
排序算法的排序趟数和原式序列有关的是交换类排序
直接插入排序和折半插入排序的比较:查找插入位置的方式不同,直接插入排序是按顺序查找的方式,而折半插入排序是按折半查找的方式。