概述
排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
我们这里说说八大排序就是内部排序。
当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
1.插入排序—直接插入排序(Straight Insertion Sort)
基本思想:
将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
要点:设立哨兵,作为临时存储和判断数组边界之用。
直接插入排序示例:
如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
算法实现(Java):
package jianzhioffer;
import java.util.Scanner;
public class ZhiJieCharuSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String str = sc.nextLine();
String[] strarray = str.split(" ");
int[] num = new int[strarray.length];
for (int i = 0; i < strarray.length; i++) {
num[i] = Integer.parseInt(strarray[i]);
}
int[] result = InsertSort(num);
for (int i = 0; i < result.length; i++) {
System.out.print(result[i]);
if (i!=result.length-1){
System.out.print(",");
}
}
}
sc.hasNext();
}
private static int[] InsertSort(int[] num) {
for (int i = 1; i < num.length; i++) {
//数组中从第0个元素到第i-1个元素都是有序的。如果待插入元素比第i-1个元素大则无需再与第i-1之前的元素比较,否则进入if语句
if (num[i] < num[i - 1]) {
//保存第i位的值
int temp = num[i];
int j;
//从第i-1位向前遍历并移位,直到找到小于第i位
for (j = i-1; j >=0&&num[j]>temp ; j--) {
//把比temp大或相等的元素全部全部往后移动一个位置
num[j+1] = num[j];
}
num[j+1] = temp;
}
//输出每一趟变换
for (int k = 0; k < num.length; k++) {
System.out.print(num[k]);
if (k!=num.length-1){
System.out.print(",");
}
}
System.out.println();
}
return num;
}
}
效率:
时间复杂度:O(n^2).
其他的插入排序有二分插入排序,2-路插入排序。
2. 插入排序—希尔排序(Shell`s Sort)
希尔排序是1959 年由D.L.Shell 提出来的,相对直接排序有较大的改进。希尔排序又叫缩小增量排序
基本思想:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
操作方法:
选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
按增量序列个数k,对序列进行k 趟排序;
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
希尔排序的示例:
算法实现(Java):
我们简单处理增量序列:增量序列d = {n/2 ,n/4, n/8 .....1} n为要排序数的个数
即:先将要排序的一组记录按某个增量d(n/2,n为要排序数的个数)分成若干组子序列,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。
package jianzhioffer;
import java.util.Scanner;
public class ShellSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String str = sc.nextLine();
String[] strarray = str.split(" ");
int[] num = new int[strarray.length];
for (int i = 0; i < strarray.length; i++) {
num[i] = Integer.parseInt(strarray[i]);
}
int[] result = shellSort(num);
for (int i = 0; i < result.length; i++) {
System.out.print(result[i]);
if (i != result.length - 1) {
System.out.print(",");
}
}
}
sc.hasNext();
}
private static int[] shellSort(int[] num) {
int j = 0;
int temp = 0;
for (int increment = num.length / 2; increment > 0; increment /= 2) {
//System.out.println("increment:"+increment);
for (int i = increment; i < num.length; i++) {
//System.out.println("i:"+i);
temp = num[i];
for (j = i - increment; j >= 0; j -= increment) {
//System.out.println("j:"+j);
//System.out.println("temp:"+temp);
//System.out.println("num["+j+"]:"+num[j]);
if (temp < num[j]) {
num[j + increment] = num[j];
} else {
break;
}
}
num[j + increment] = temp;
}
}
return num;
}
}
复杂度分析:
希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式移动,使得排序的效率提高。需要注意的是,增量序列的最后一个增量值必须等于1才行。另外,由于记录是跳跃式的移动,希尔排序并不是一种稳定的排序算法。
希尔排序最好时间复杂度和平均时间复杂度都是,最坏时间复杂度为。
3. 选择排序—简单选择排序(Simple Selection Sort)
基本思想:
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
简单选择排序的示例:
操作方法:
第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;
第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;
以此类推.....
第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换,
直到整个序列按关键码有序。
package jianzhioffer;
import java.util.Scanner;
public class SimpleSelectionSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String str = sc.nextLine();
String[] strarray = str.split(" ");
int[] num = new int[strarray.length];
for (int i = 0; i < strarray.length; i++) {
num[i] = Integer.parseInt(strarray[i]);
}
int[] result = simpleSelectionSort(num);
for (int i = 0; i < result.length; i++) {
System.out.print(result[i]);
if (i != result.length - 1) {
System.out.print(",");
}
}
}
sc.hasNext();
}
private static int[] simpleSelectionSort(int[] num) {
int len = num.length;
for (int i = 0; i < len; i++) {
//记录当前位置
int position = i;
//找出最小的数,并用position指向最小数的位置
for (int j = i+1; j < len; j++) {
if (num[position]>num[j]){
position = j;
}
}
//交换最小数data[position]和第i位数的位置
if (position!=i){
int temp = num[i];
num[i] = num[position];
num[position] = temp;
}
}
return num;
}
}
简单选择排序的改进——二元选择排序
简单选择排序,每趟循环只能确定一个元素排序后的定位。我们可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可。
具体实现如下:
/**
* 二元选择排序
* @param num:待排序的数组
* @return
*/
private static int[] simpleSelectionSort(int[] num) {
for (int i = 0; i < num.length/2; i++) {
int min = i;
int max = i;
for (int j = i; j < num.length-i; j++) {
if (num[j]<num[min]){
min = j;
continue;
}
if (num[j]>num[max]){
max = j;
}
}
//将最小值放在第i处,将最大值放在第num.length-i-1处
//不能把num[max]、num[min]直接和num[i]、num[num.length-i-1]调换
int maxtemp =num[max];
int mintemp = num[min];
num[max] = num[num.length-i-1];
num[min] = num[i];
num[i] = mintemp;
num[num.length-i-1] = maxtemp;
}
return num;
}
复杂度分析:
在简单选择排序过程中,所需移动记录的次数比较少。最好情况下,即待排序记录初始状态就已经是正序排列了,则不需要移动记录。
最坏情况下,即待排序记录初始状态是按第一条记录最大,之后的记录从小到大顺序排列,则需要移动记录的次数最多为3(n-1)。简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排列情况无关。当i=1时,需进行n-1次比较;当i=2时,需进行n-2次比较;依次类推,共需要进行的比较次数是(n-1)+(n-2)+…+2+1=n(n-1)/2,即进行比较操作的时间复杂度为O(n^2),进行移动操作的时间复杂度为O(n)。
简单选择排序是不稳定排序。
4. 选择排序—堆排序(Heap Sort)
堆排序是一种树形选择排序,是对直接选择排序的有效改进。
基本思想:
堆的定义如下:具有n个元素的序列(k1,k2,...,kn),当且仅当满足
时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:
1)大顶堆序列:(96, 83,27,38,11,09)
2)小顶堆序列:(12,36,24,85,47,30,53,91)
初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。
因此,实现堆排序需解决两个问题:
1. 如何将n 个待排序的数建成堆;
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。
首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。
调整小顶堆的方法:
1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。
2)将根结点与左、右子树中较小元素的进行交换。
3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).
4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).
5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。
称这个自根结点到叶子结点的调整过程为筛选。如图:
再讨论对n 个元素初始建堆的过程。
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。
1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。
2)筛选从第个结点为根的子树开始,该子树成为堆。
3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。
如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)
算法的实现:
从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
package jianzhioffer;
import java.util.Scanner;
/**
* 选择排序——堆排序
*/
public class HeapSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String str = sc.nextLine();
String[] strarray = str.split(" ");
int[] num = new int[strarray.length];
for (int i = 0; i < strarray.length; i++) {
num[i] = Integer.parseInt(strarray[i]);
}
int[] result = heapSort(num);
for (int i = 0; i < result.length; i++) {
System.out.print(result[i]);
if (i != result.length - 1) {
System.out.print(",");
}
}
}
sc.hasNext();
}
private static int[] heapSort(int[] num) {
//构建大顶堆
for (int i = num.length / 2 - 1; i >= 0; i--) {
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(num, i, num.length);
}
//调整堆结构,交换堆顶元素与末尾元素
for (int i = num.length - 1; i > 0; i--) {
swap(num, 0, i);//将堆顶元素与末尾元素进行交换
adjustHeap(num, 0, i);//重新对堆进行调整
}
return num;
}
/**
* 交换元素
*
* @param num
* @param i
* @param i1
*/
private static void swap(int[] num, int a, int b) {
int temp = num[a];
num[a] = num[b];
num[b] = temp;
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
*
* @param num
* @param i
* @param length
*/
private static void adjustHeap(int[] num, int i, int length) {
int temp = num[i];//取出当前元素i
//从i结点的左子节点开始,也就是2*i+1处开始
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
//如果左子节点小于右子节点,k指向右子节点
if (k + 1 < length && num[k] < num[k + 1]) {
k++;
}
//如果子节点大于父结点,将子节点值赋给父结点(不用进行交换)
if (num[k] > temp) {
num[i] = num[k];
i = k;
} else {
break;
}
}
//将temp值放到最终的位置
num[i] = temp;
}
}
复杂度分析:
设树深度为k,。从根到叶的筛选,元素比较次数至多2(k-1)次,交换记录至多k 次。所以,在建好堆后,排序过程中的筛选次数不超过下式:
而建堆时的比较次数不超过4n 次,因此堆排序最坏情况下,时间复杂度也为:O(nlogn )。
5. 交换排序—冒泡排序(Bubble Sort)
基本思想:
在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
冒泡排序的示例:
算法实现(Java):
/**
* 基本冒泡排序
* @param num
* @return
*/
private static int[] bubbleSort(int[] num) {
for (int i = 0; i < num.length-1; i++) {
for (int j = 0; j < num.length-i-1; j++) {
if (num[j]>num[j+1]){
//交换
int temp = num[j];
num[j] = num[j+1];
num[j+1] = temp;
}
}
}
return num;
}
冒泡排序算法的改进
对冒泡排序常见的改进方法是加入一标志性变量exchange,用于标志某一趟排序过程中是否有数据交换,如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程。本文再提供以下两种改进算法:
1.设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。
改进后算法如下:
/**
* 改进1:设置标志变量pos,记录每趟排序中最后一次交换的位置
* @param num
* @return
*/
private static int[] bubbleSort(int[] num) {
int i = num.length-1;
while (i>0){
int pos = 0;//每趟开始,无记录交换
for (int j = 0; j < i; j++) {
if (num[j]>num[j+1]){
pos = j;//记录交换的位置
int temp = num[j];
num[j] = num[j+1];
num[j+1] = temp;
}
}
i = pos;
}
return num;
}
2.传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者) , 从而使排序趟数几乎减少了一半。
改进后的算法实现为:
/**
* 改进2:利用每趟排序中进行正向和反向两遍冒泡的方法一次得到两个最终值
* @param num
* @return
*/
private static int[] bubbleSort(int[] num) {
int low = 0;
int high = num.length-1;
int temp,j;
while (low<high){
//正向冒泡,找到最大者
for (j = low; j < high; j++) {
if (num[j]>num[j+1]){
temp = num[j];
num[j] = num[j+1];
num[j+1] = temp;
}
}
//修改high的值,前移一位
high--;
for (j = high; j > low; j--) {
if (num[j]<num[j-1]){
temp = num[j];
num[j] = num[j-1];
num[j-1] = temp;
}
}
low++;
}
return num;
}
复杂度分析
时间复杂度
若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数M均达到最小值:
, 。
所以,冒泡排序最好的时间复杂度为 。
若初始文件是反序的,需要进行 趟排序。每趟排序要进行 次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
冒泡排序的最坏时间复杂度为 。
综上,因此冒泡排序总的平均时间复杂度为 。
算法稳定性
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
6. 交换排序—快速排序(Quick Sort)
基本思想:
1)选择一个基准元素,通常选择第一个元素或者最后一个元素,
2)通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。
3)此时基准元素在其排好序后的正确位置
4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。
快速排序的示例:
(a)一趟排序的过程:
(b)排序的全过程
算法的实现:
递归实现:
package jianzhioffer;
import java.util.Arrays;
import java.util.Scanner;
/**
* 快速排序
*/
public class QuickSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String str = sc.nextLine();
String[] strarray = str.split(" ");
int[] num = new int[strarray.length];
for (int i = 0; i < strarray.length; i++) {
num[i] = Integer.parseInt(strarray[i]);
}
quickSort(num);
// for (int i = 0; i < result.length; i++) {
// System.out.print(result[i]);
// if (i != result.length - 1) {
// System.out.print(",");
// }
// }
System.out.println(Arrays.toString(num));
}
sc.hasNext();
}
/**
* 快速排序
* @param num
* @return
*/
private static void quickSort(int[] num) {
recurPartion(num,0,num.length-1);
}
/**
* 快速排序
* @param i
* @param i1
* @param num
* @return
*/
private static void recurPartion(int[] num, int low, int high) {
//递归调用的结束条件,开始要拆分的数组就剩下一个元素的时候
if (low-high<1)
return;
int part = partition(num,low,high);
//三种情况下继续拆分
if (part == low)
recurPartion(num,part+1,high);
else if (part==high)
recurPartion(num,low,high-1);
else{
recurPartion(num,low,part-1);
recurPartion(num,part+1,high);
}
}
/**
* 数组切分
* @param num 要拆分的数组
* @param low 数组拆分的起始索引(从0开始)
* @param high 数组拆分的结束索引
* @return
*/
private static int partition(int[] num, int low, int high) {
//选取基准元素,以最后一个位置,作为基准
int base = num[high];
//记录比基准元素小的变量
//这里我们假设要比较的元素都不小于基准元素,这样通过比较就把小于基准元素的数据全部找到
//n=low表示的就是默认没有小于基准元素
int n =low;
//基准元素不参与遍历比较
for (int i = low; i < high; i++) {
if (num[i]<base){
//将小于基准元素的放到基准的左边
if (i!=n)
swap(num,i,n);
n++;
}
}
//遍历完成之后,将基准元素放到应该的位置上
swap(num,n,high);
return n;
}
/**
* 交换数组中指定位置的两个元素
* @param a
* @param b
* @param num
*/
private static void swap(int[] num, int a, int b) {
int temp = num[a];
num[a] = num[b];
num[b] = temp;
}
}
分析:
快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是一个不稳定的排序方法。
快速排序的改进
在本改进算法中,只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。算法思想如下:
package jianzhioffer;
import java.util.Arrays;
import java.util.Scanner;
/**
* 快速排序的改进
* 最好情况下,基准元素应该是所有元素的平均值,即中值,这样就更接近归并排序的切分情况。
* 但是前面的三种partition实现都是选取的第一个元素为基准元素,并不能有这个保证,
* 采取三数中值法(三取样切分),比较low,mid,high的大小,选取中间的一个作为基准元素。
*/
public class QuickSort4 {
/*
若数组大小不超过CUTOFF,则切换到插入排序
*/
private static final int CUTOFF = 10;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String str = sc.nextLine();
String[] strarray = str.split(" ");
int[] num = new int[strarray.length];
for (int i = 0; i < strarray.length; i++) {
num[i] = Integer.parseInt(strarray[i]);
}
quickSort4(num);
// for (int i = 0; i < result.length; i++) {
// System.out.print(result[i]);
// if (i != result.length - 1) {
// System.out.print(",");
// }
// }
System.out.println(Arrays.toString(num));
}
sc.hasNext();
}
private static void quickSort4(int[] num) {
quickSort4(num, 0, num.length - 1);
}
private static void quickSort4(int[] num, int low, int high) {
//切换到插入排序,调用插入排序后直接返回
if (low + CUTOFF >= high) {
insertionSort(num);
return;
}
if (low >= high)
return;
//将三取样的中值和low交换
int m = threeMedium(num, low, low + (high - low) / 2, high);
swap(num, m, low);
int position = partition(num, low, high);
quickSort4(num, low, position - 1);
quickSort4(num, position + 1, high);
}
private static void insertionSort(int[] num) {
for (int i = 0; i < num.length; i++) {
for (int j = i; j > 0 && num[j] < num[j - 1]; j--) {
swap(num, j, j - 1);
}
}
}
private static void swap(int[] num, int m, int low) {
int temp = num[m];
num[m] = num[low];
num[low] = temp;
}
private static int threeMedium(int[] num, int low, int mid, int high) {
return (num[low] < num[mid] ? (num[mid] < num[high] ? mid : (num[low] < num[high] ? high : low)) : (num[low] < num[high] ? low : (num[mid] < num[high] ? high : mid)));
}
private static int partition(int[] num, int low, int high) {
int pivot = num[low];
while (low < high) {
while (low < high && num[high] >= pivot)
high--;
num[low] = num[high];
while (low < high && num[low] <= pivot)
low++;
num[high] = num[low];
}
num[low] = pivot;
return low;
}
}
复杂度分析
时间复杂度
快速排序和归并排序一样,都使用了递归,故可以借助递归公式来分析。对于一个未采取插入排序转换和三取样切分的快速排序,其运行时间等于两个子数列排序的时间加上在切分上花费的时间,因为扫描,显然切分花费的时间与数组规模正相关,故得到地推公式:
T(N)= T(i)+T(N-i-1)+cN
N为数组规模,i为切分后其中较小一部分的元素个数,c为某一常数
(1)最坏情况
基准元素始终是最小元素,此时i始终为0,T(0)=T(1)=1,与问题规模无关,在一个递推公式中可以忽略掉,故得到:T(N)=T(N-1)+cN,反复使用该公式,直到N为2,然后累加。
(2)最好情况
最好情况下,基准元素是中数,为简化分析,假设两个子数组大小均为原数组的一半,分析和归并排序类似。
最好情况下快速排序时间复杂度分析:
(3)平均情况
平均情况下快速排序的时间复杂度分析:
每个半部分花费的平均时间为:
每个半部分的长度可能为0到N-1之间,修改后的递归公式为:
两边乘以N,得
空间复杂度
在最好情况和平均情况下,sort递归的次数是log2N次,partition返回的基准元素的位置的局部变量所占用得空间就是log2N次,partition函数里面的局部变量也是与log2N成正比,即空间复杂度是O(log2N);在最坏情况下,sort递归次数是N^2,此时的空间复杂度将是O(N^2),但是这样的概率很小,经过三取样后就减小了,如果排序前打乱数组,那么这种情况出现的概率可以忽略不计,证明请参考《Algorithms Fourth Edition》。
所以快速排序的空间复杂度为O(log2N)
稳定性
不稳定有两个地方,第一个地方是在交换pivot前面大于pivot的元素和pivot后面小于pivot的元素,第二地方在partition函数返回前将基准元素放到正确位置,若待放位置前有和基准元素值相等的元素,则破坏了稳定性。
7. 归并排序(Merge Sort)
基本思想:
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序示例:
合并方法:
设r[i…n]由两个有序子表r[i…m]和r[m+1…n]组成,两个子表长度分别为n-i +1、n-m。
j=m+1;k=i;i=i; //置两个子表的起始下标及辅助数组的起始下标
若i>m 或j>n,转⑷ //其中一个子表已合并完,比较选取结束
//选取r[i]和r[j]较小的存入辅助数组rf
如果r[i]<r[j],rf[k]=r[i]; i++; k++; 转⑵
否则,rf[k]=r[j]; j++; k++; 转⑵
//将尚未处理完的子表中元素存入rf
如果i<=m,将r[i…m]存入rf[k…n] //前一子表非空
如果j<=n , 将r[j…n] 存入rf[k…n] //后一子表非空
合并结束。
归并的迭代算法:
package jianzhioffer;
import java.util.Arrays;
import java.util.Scanner;
public class MergeSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String str = sc.nextLine();
String[] strarray = str.split(" ");
int[] num = new int[strarray.length];
for (int i = 0; i < strarray.length; i++) {
num[i] = Integer.parseInt(strarray[i]);
}
mergeSort(num);
// for (int i = 0; i < result.length; i++) {
// System.out.print(result[i]);
// if (i != result.length - 1) {
// System.out.print(",");
// }
// }
System.out.println(Arrays.toString(num));
}
sc.hasNext();
}
private static void mergeSort(int[] num) {
mergeSort(num, 0, num.length - 1);
}
private static void mergeSort(int[] num, int left, int right) {
if (left >= right)
return;
//找出中间索引
int center = (left + right) / 2;
//对左边数组进行递归
mergeSort(num, left, center);
//对右边数组进行递归
mergeSort(num, center + 1, right);
//合并
merge(num, left, center, right);
}
/**
* 将两个数组进行归并,归并前面2个数组已有序,归并后依然有序
*
* @param num 数组对象
* @param left 左数组的第一个元素的索引
* @param center 左数组的最后一个元素的索引,center+1是右数组第一个元素的索引
* @param right 右数组最后一个元素的索引
*/
private static void merge(int[] num, int left, int center, int right) {
//临时数组
int[] tempArray = new int[num.length];
//右数组第一个元素索引
int mid = center + 1;
//third记录临时数组的索引
int third = left;
//缓存左数组第一个元素的索引
int temp = left;
while (left <= center && mid <= right) {
//从两个数组中取出最小的放入临时数组
if (num[left] <= num[mid]) {
tempArray[third++] = num[left++];
} else {
tempArray[third++] = num[mid++];
}
}
//剩余部分依次放入临时数组(实际上两个while只会执行其中一个)
while (mid <= right) {
tempArray[third++] = num[mid++];
}
while (left <= center) {
tempArray[third++] = num[left++];
}
//将临时数组中的内容拷贝回原数组中
//(原left-righe范围的内容被复制回原数组)
while (temp <= right) {
num[temp] = tempArray[temp++];
}
}
}
复杂度分析
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
归并排序算法稳定,数组需要O(n)的额外空间,链表需要O(log(n))的额外空间,时间复杂度为O(nlog(n)),算法不是自适应的,不需要对数据的随机读取。
8. 桶排序/基数排序(Radix Sort)
说基数排序之前,我们先说桶排序:
基本思想
将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,它不受到 O(n log n) 下限的影响。
简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。
例如要对大小为[1..1000]范围内的n个整数A[1..n]排序
首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储 (10..20]的整数,……集合B[i]存储( (i-1)*10, i*10]的整数,i = 1,2,..100。总共有 100个桶。
然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。
最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是
从上式看出,当m接近n的时候,桶排序复杂度接近O(n)
当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。
前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:
1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。
2)其次待排序的元素都要在一定的范围内等等。
桶式排序是一种分配排序。分配排序的特定是不需要进行关键码的比较,但前提是要知道待排序列的一些具体情况。
分配排序的基本思想:说白了就是进行多次的桶式排序。
基数排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序。它们的时间复杂度可达到线性阶:O(n)。
实例:
扑克牌中52 张牌,可按花色和面值分成两个字段,其大小关系为:
花色: 梅花< 方块< 红心< 黑心
面值: 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < J < Q < K < A
若对扑克牌按花色、面值进行升序排序,得到如下序列:
即两张牌,若花色不同,不论面值怎样,花色低的那张牌小于花色高的,只有在同花色情况下,大小关系才由面值的大小确定。这就是多关键码排序。
为得到排序结果,我们讨论两种排序方法。
方法1:先对花色排序,将其分为4 个组,即梅花组、方块组、红心组、黑心组。再对每个组分别按面值进行排序,最后,将4 个组连接起来即可。
方法2:先按13 个面值给出13 个编号组(2 号,3 号,...,A 号),将牌按面值依次放入对应的编号组,分成13 堆。再按花色给出4 个编号组(梅花、方块、红心、黑心),将2号组中牌取出分别放入对应花色组,再将3 号组中牌取出分别放入对应花色组,……,这样,4 个花色组中均按面值有序,然后,将4 个花色组依次连接起来即可。
设n 个元素的待排序列包含d 个关键码{k1,k2,…,kd},则称序列对关键码{k1,k2,…,kd}有序是指:对于序列中任两个记录r[i]和r[j](1≤i≤j≤n)都满足下列有序关系:
其中k1 称为最主位关键码,kd 称为最次位关键码。
两种多关键码排序方法:
多关键码排序按照从最主位关键码到最次位关键码或从最次位到最主位关键码的顺序逐次排序,分两种方法:
最高位优先(Most Significant Digit first)法,简称MSD 法:
1)先按k1 排序分组,将序列分成若干子序列,同一组序列的记录中,关键码k1 相等。
2)再对各组按k2 排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd 对各子组排序后。
3)再将各组连接起来,便得到一个有序序列。扑克牌按花色、面值排序中介绍的方法一即是MSD 法。
最低位优先(Least Significant Digit first)法,简称LSD 法:
1) 先从kd 开始排序,再对kd-1进行排序,依次重复,直到按k1排序分组分成最小的子序列后。
2) 最后将各个子序列连接起来,便可得到一个有序的序列, 扑克牌按花色、面值排序中介绍的方法二即是LSD 法。
基于LSD方法的链式基数排序的基本思想
“多关键字排序”的思想实现“单关键字排序”。对数字型或字符型的单关键字,可以看作由多个数位或多个字符构成的多关键字,此时可以采用“分配-收集”的方法进行排序,这一过程称作基数排序法,其中每个数字或字符可能的取值个数称为基数。比如,扑克牌的花色基数为4,面值基数为13。在整理扑克牌时,既可以先按花色整理,也可以先按面值整理。按花色整理时,先按红、黑、方、花的顺序分成4摞(分配),再按此顺序再叠放在一起(收集),然后按面值的顺序分成13摞(分配),再按此顺序叠放在一起(收集),如此进行二次分配和收集即可将扑克牌排列有序。
基数排序:
是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
1、基数排序用于解决待排序记录数目较大,但记录关键字维数较小(如关键字是整数,则维数为10;若关键是字符串,则维数为26);
2、基数排序由分配和收集两部分组成;
3、基数排序实现的关键是下一趟的分配操作要在上一趟的收集结果上进行(分配和收集过程中均借助了单链表操作)。
算法实现:
package jianzhioffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
public class RadixSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String str = sc.nextLine();
String[] strarray = str.split(" ");
int[] num = new int[strarray.length];
for (int i = 0; i < strarray.length; i++) {
num[i] = Integer.parseInt(strarray[i]);
}
radixSort(num);
// for (int i = 0; i < result.length; i++) {
// System.out.print(result[i]);
// if (i != result.length - 1) {
// System.out.print(",");
// }
// }
System.out.println(Arrays.toString(num));
}
sc.hasNext();
}
private static void radixSort(int[] num) {
//首先确定排序的趟数
int max = num[0];
for (int i = 0; i < num.length; i++) {
if (num[i]>max){
max = num[i];
}
}
int time = 0;
//判断位数
while (max>0){
max/=10;
time++;
}
//建立10个队列
List<ArrayList> queue = new ArrayList<ArrayList>();
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 < num.length; j++) {
//得到数字的第time+1位数
int x = num[j]%(int)Math.pow(10,i+1)/(int)Math.pow(10,i);
ArrayList<Integer> queue2 = queue.get(x);
queue2.add(num[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);
num[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
}
总结:
各种排序的稳定性,时间复杂度和空间复杂度总结:
我们比较时间复杂度函数的情况:
时间复杂度函数O(n)的增长情况
所以对n较大的排序记录。一般的选择都是时间复杂度为O(nlog2n)的排序方法。
时间复杂度来说:
(1)平方阶(O(n2))排序
各类简单排序:直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlog2n))排序
快速排序、堆排序和归并排序;
(3)O(n1+§))排序,§是介于0和1之间的常数。
希尔排序
(4)线性阶(O(n))排序
基数排序,此外还有桶、箱排序。
说明:
当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。
稳定性:
排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。
稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较;
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
选择排序算法准则:
每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。
选择排序算法的依据:
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。
设待排序元素的个数为n.
1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序 : 如果内存空间允许且要求稳定性的,
归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
2) 当n较大,内存空间允许,且要求稳定性 =》归并排序
3)当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
5)一般不使用或不直接使用传统的冒泡排序。
6)基数排序
它是一种稳定的排序算法,但有一定的局限性:
1、关键字可分解。
2、记录的关键字位数较少,如果密集更好
3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。