首先,我们先说说排序的分类和特性:
一、排序的分类
1. 内部排序和外部排序
(1) 内部:待排序记录存放在计算机随机存储器(内存)中进行的排序过程。
(2) 外部:待排序记录的数量很大,以致于内存不能一次容纳全部记录,所以在排序过程中需要对外存进行访问的排序过程。
2. 比较和非比较排序
(1)比较类:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
(2)非比较类:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
二、特性
3. 稳定和非稳定性
(1)稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
(2)非稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面,也即原来的相对位置改变。
4. 复杂度:
(1)时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
(2)是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
三、八大排序讲解
好了,不再多说了,开始入正题:
首先,看八大排序,每个算法的特性:
交换排序:冒泡和快排
1.冒泡排序
(1) 动画演示:
(2) 算法思路分析:
1>相邻两个数两两相比,n[j]跟n[j+1]比,如果n[j]>n[j+1],则将两个数进行交换,
2> j++, 重复以上步骤,第一趟结束后,最大数就会被确定在最后一位,这就是冒泡排序又称大(小)数沉底,
3>i++,重复以上步骤,直到i=n-1结束,排序完成。
(3) 复杂度分析:
1> 时间复杂度:
不管原始数组是否有序,时间复杂度都是O(n2),因为没一个数都要与其他数比较一次,(n-1)2次,分解:n2+2n-1, 去掉低次幂和常数,剩下n2,所以最后的时间复杂度是n2。
2>空间复杂度:因为只定义了一个辅助变量,与n的大小无关,所以空间复杂度为O(1)。
(4) java代码:
import java.util.Scanner;
public class Bubbling {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int temp;
int [] data = new int [len];
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
//排序
for(int i = 0; i < len; i++) {
for(int j = 0; j < len - 1 - i; j++) {
if(data[j] > data[j + 1]) {
temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
}
}
}
//输出
for(int i = 0; i < len; i++) {
System.out.print(data[i]);
}
sca.close();
}
}
import java.util.Scanner;
//冒泡排序演变而来
public class 快速 {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
quickSort(data, 0, data.length - 1);//快排
for(int i = 0; i < data.length; i++) {
System.out.print(data[i] + " ");
}
sca.close();
}
public static void quickSort(int [] data, int left, int right) {
int f, t;
int rtemp, ltemp;
ltemp = left; //左指针
rtemp = right; //右指针
f = data[(left + right) / 2]; //传来的每一个子表的中间值
while(ltemp < rtemp) { //左指针只要比右边小就开始循环
while(data[ltemp] < f) ++ltemp; //从左边开始,只要当前数小于中间值,保留右移
while(data[rtemp] > f) --rtemp; //从右边开始,只要当前数大于中间值,保留左移
if(ltemp <= rtemp) { //左指针小于等于右指针(等于 ==>是为避免子表只有俩元素)
t = data[ltemp];
data[ltemp] = data[rtemp];
data[rtemp] = t;
--rtemp;
++ltemp;
}
}
if(ltemp == rtemp) ltemp++; //左右指针相等,左指针右移(左右指针都移到了中间)
if(left < rtemp) quickSort(data, left, ltemp - 1); //右指针没有到达左头部,以左指针为右头部形成左子表
if(ltemp < right) quickSort(data, rtemp + 1, right);//左指针没有到达右头部,以右指针为左头部形成右子表
}
}
插入排序:简单插入排序和希尔排序
1. 简单插入排序:
(1) 动画演示:
(2) 算法思路分析:
如:从小到大排序:
1> 从第二位开始遍历,
2> 当前数(第一趟是第二位数)与前面的数依次比较,如果前面的数大于当前数,则将这个数放在当前数的位置上,当前数的下标-1,
3> 重复以上步骤,直到当前数不大于前面的某一个数为止,这时,将当前数,放到这个位置,
**注:**1-3步就是保证当前数的前面的数都是有序的,内层循环的目的就是将当前数插入到前面的有序序列里
4> 重复以上3步,直到遍历到最后一位数,并将最后一位数插入到合适的位置,插入排序结束。
下面用图,模拟算法每一趟执行流程:
(3) 复杂度分析:
1>时间复杂度:插入算法,就是保证前面的序列是有序的,只需要把当前数插入前面的某一个位置即可。所以如果数组本来就是有序的,则数组的最好情况下时间复杂度为O(n) 如果数组恰好是倒=倒序,比如原始数组是5 4 3 2 1,想要排成从小到大,则每一趟前面的数都要往后移,一共要执行n-1 + n-2 + … + 2 + 1 = n * (n-1) / 2 = 0.5 * n2 - 0.5 * n次,去掉低次幂及系数,所以最坏情况下时间复杂度为O(n2)。
平均时间复杂度(n+n2 )/2,所以平均时间复杂度为O(n2)
2>空间复杂度:
插入排序算法,只需要两个变量暂存当前数,以及下标,与n的大小无关,所以空间复杂度为:O(1)
(4) java代码:
import java.util.Scanner;
public class Insert{
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
int j, t;
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
for(int i = 1; i < data.length; i++) {
t = data[i];
j = i - 1;
while(j >= 0 && t < data[j]) { //比当前扫描位置小 并且没有到数组头部,继续往前扫描
data[j + 1] = data[j]; //让上一个单元,等于当前扫描单元的值
j--; //继续往前扫描
}
data[j + 1] = t; //让当前扫描位置的上一个单元等于所要插入的元素
}
for(int i = 0; i < data.length; i++) {
System.out.print(data[i] + " ");
}
sca.close();
}
}
2.希尔排序:
(1) 动画演示:
(2) 算法思想分析:
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。
而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
那么来看下希尔排序的基本步骤,在此选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处做示例使用希尔增量。
(3) 复杂度分析:
1> 时间复杂度:最坏情况下,每两个数都要比较并交换一次,则最坏情况下的时间复杂度为O(n2), 最好情况下,数组是有序的,不需要交换,只需要比较,则最好情况下的时间复杂度为O(n)。
2> 希尔排序,只需要一个变量用于两数交换,与n的大小无关,所以空间复杂度为:O(1)。
选择排序:简单选择排序和堆排序
(4) java代码:
import java.util.Scanner;
//插入排序演变而来,缩小增量
public class 希尔 {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
int i, j;
int r, temp;
for(i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
for(r = data.length / 2; r >= 1; r /= 2) { //增量 每次缩减一半
for(i = r; i < data.length; i++) { //让这个增量所在的序列对进行比较(序列对排序就是插入排序)
temp = data[i];
j = i - r; //每次 + 1 的在 '本序列对' 进行扫描
while(j >= 0 && temp < data[j]) {
data[j + r] = data[j];
j -= r;
}
data[j + r] = temp;
}
}
for(int k = 0; k < data.length; k++) {
System.out.print(data[k] + " ");
}
sca.close();
}
}
1.简单选择排序:
(1)动画演示:
(2) 算法思路分析:
1> 第一个跟后面的所有数相比,如果小于(或小于)第一个数的时候,暂存较小数的下标,第一趟结束后,将第一个数,与暂存的那个最小数进行交换,第一个数就是最小(或最大的数)
2> 下标移到第二位,第二个数跟后面的所有数相比,一趟下来,确定第二小(或第二大)的数
3> 重复以上步骤,直到指针移到倒数第二位,确定倒数第二小(或倒数第二大)的数,那么最后一位也就确定了,排序完成。
(3) 复杂度分析:
1>时间复杂度:不管原始数组是否有序,时间复杂度都是O(n2),因为没一个数都要与其他数比较一次,(n-1)2次,分解:n2-2n+1, 去掉低次幂和常数,剩下n2,所以最后的时间复杂度是n2。
2>空间复杂度:因为只定义了两个辅助变量,与n的大小无关,所以空间复杂度为O(1)。
(4) java代码:
import java.util.Scanner;
public class Choice {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
int index, temp;
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
for(int i = 0; i < len - 1; i++) {
index = i;
for(int j = i + 1; j < len; j++) {
if(data[j] < data[index]) {
index = j;
}
}
if(index != i) {
temp = data[i];
data[i] = data[index];
data[index] = temp;
}
}
for(int i = 0; i < len; i++) {
System.out.print(data[i] + " ");
}
sca.close();
}
}
b.重新调整结构,使其继续满足堆定义
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
(3) 再简单总结下堆排序的基本思路:
a.将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
(4) 复杂度分析:
1> 时间复杂度:堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度最好和最坏情况下都是O(nlogn)级。
2> 堆排序不要任何辅助数组,只需要一个辅助变量,所占空间是常数与n无关,所以空间复杂度为O(1)。
(5) java代码:
import java.util.Arrays;
import java.util.Scanner;
//选择排序思想演变而来
public class Heap{
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
heapSort(data);
System.out.println(Arrays.toString(data));
sca.close();
}
public static void heapSort(int [] data) {
int len = data.length;
//开始位置是最后一个非叶子节点,即最后一个节点的父节点(二叉树特性)
int start = (len - 1) / 2;
//初始化一个大顶堆(以每一个节点形成的堆都是一个大顶堆)
for(int i = start; i >= 0; i--) {
creatMaxHeap(data, len, i);
}
//先把数组中的第0个和堆中的最后一个数交换位置,再把前面的处理为大顶堆
for(int i = len - 1; i > 0; i--) {
int temp = data[0];
data[0] = data[i];
data[i] = temp;
creatMaxHeap(data, i, 0);
}
}
public static void creatMaxHeap(int [] data, int len, int index) {
//左子节点
int leftNode = 2 * index + 1;
//右子节点
int rightNode = 2 * index + 2;
int max = index;
//和两个子节点分别对比,找出最大的节点
if(leftNode < len && data[leftNode] > data[max]) max = leftNode;
if(rightNode < len && data[rightNode] > data[max]) max = rightNode;
//交换位置
if(max != index) {
int temp = data[index];
data[index] = data[max];
data[max] = temp;
//交换位置以后,可能会破坏之前排好的堆,所以,之前排好的堆需要重新调整
creatMaxHeap(data, len, max);
}
}
}
归并排序:二路归并和多路归并(这里不再叙述)
1.二路归并
(1) 动画演示:
(2) 算法思想分析:
归并排序就是递归得将原始数组递归对半分隔,直到不能再分(只剩下一个元素)后,开始从最小的数组向上归并排序
1> 向上归并排序的时候,需要一个暂存数组用来排序,
2> 将待合并的两个数组,从第一位开始比较,小的放到暂存数组,指针向后移,
3> 直到一个数组空,这时,不用判断哪个数组空了,直接将两个数组剩下的元素追加到暂存数组里,
4> 再将暂存数组排序后的元素放到原数组里,两个数组合成一个,这一趟结束。
根据思路分析,每一趟的执行流程如下图所示:
(3)算法复杂度分析:
1>时间复杂度:递归算法的时间复杂度公式:T[n] = aT[n/b] + f(n)
无论原始数组是否是有序的,都要递归分隔并向上归并排序,所以时间复杂度始终是O(nlog2n)2>空间复杂度:
每次两个数组进行归并排序的时候,都会利用一个长度为n的数组作为辅助数组用于保存合并序列,所以空间复杂度为O(n)。
(4) java代码:
import java.util.Arrays;
import java.util.Scanner;
public class Merge {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
System.out.println(Arrays.toString(data));
mergeSort(data, 0, data.length - 1);
System.out.println(Arrays.toString(data));
sca.close();
}
public static void mergeSort(int [] data, int low, int high) {
int middle = (low + high) / 2;
//如果最后只剩下一个不再递归
if(low < high) {
//处理左边
mergeSort(data, low, middle);
//处理右边
mergeSort(data, middle + 1, high);
//归并
merge(data, low, middle, high);
}
}
public static void merge(int [] data, 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(data[i] < data[j]) {
//把小的数放到数组中
temp[index] = data[i];
//下标移向后一位
i++;
}else {
temp[index] = data[j];
j++;
}
index++;
}
while(i <= middle) {
temp[index] = data[i];
i++;
index++;
}
while(j <= high) {
temp[index] = data[j];
j++;
index++;
}
//把临时数组的中的数据重新存入原数组
for(int k = 0; k < temp.length; k++) {
data[k + low] = temp[k];
}
}
}
非比较排序:基排序、(桶排序、计数排序这俩个不再叙述)
(1) 动画演示:
(2) 算法思想分析:
基数排序第i趟将待排数组里的每个数的i位数放到tempj(j=1-10)队列中,然后再从这十个队列中取出数据,重新放到原数组里,直到i大于待排数的最大位数。
1.数组里的数最大位数是n位,就需要排n趟,例如数组里最大的数是3位数,则需要排3趟。
2.若数组里共有m个数,则需要十个长度为m的数组tempj(j=0-9)用来暂存i位上数为j的数,例如,第1趟,各位数为0的会被分配到temp0数组里,各位数为1的会被分配到temp1数组里…
3.分配结束后,再依次从tempj数组中取出数据,遵循先进先出原则,例如对数组{1,11,2,44,4},进行第1趟分配后,temp1={1,11},temp2={2},temp4={44,4},依次取出元素后{1,11,2,44,4},第一趟结束
4.循环到n趟后结束,排序完成。
根据思路分析,每一趟的执行流程如下图所示:
通过基数排序对数组{53, 3, 542, 748, 14, 214, 154, 63, 616}:
(3) 复杂度分析:
1>时间复杂度:每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d2n) ,当然d要远远小于n,因此基本上还是线性级别的。系数2可以省略,且无论数组是否有序,都需要从个位排到最大位数,所以时间复杂度始终为O(dn) 。其中,n是数组长度,d是最大位数。
2>空间复杂度:
基数排序的空间复杂度为O(n+k),其中k为桶的数量,需要分配n个数。
(4) java代码:
import java.util.Arrays;
import java.util.Scanner;
public class Base {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
System.out.println(Arrays.toString(data));
radixSort(data);
System.out.println(Arrays.toString(data));
sca.close();
}
public static void radixSort(int [] data) {
//存放数组中最大的数字
int max = Integer.MIN_VALUE;
for(int i = 0; i < data.length; i++) {
if(data[i] > max) {
max = data[i];
}
}
//计算最大数字是几位
int maxLength = (max + "").length();
//用于临时存储数据的数组
int [][] temp = new int [10][data.length];
//用于记录在temp中相应的数组中存放的数字数量
int [] counts = new int [10];
//根据最大长度的数,决定比较的次数
for(int i = 0, n = 1; i < maxLength; i++, n *= 10) {
//把每一个数字分别计算余数
for(int j = 0; j < data.length; j++) {
//计算余数
int ys = data[j] / n % 10;
//把当前遍历的数据放入指定的数组中
temp[ys][counts[ys]] = data[j];
//记录数量
counts[ys]++;
}
//记录取出的元素需要放的位置
int index = 0;
//把数字取出来
for(int k = 0; k < counts.length; k++) {
//记录数量的数组中当前余数记录的数量不为0
if(counts[k] != 0) {
//循环取出元素
for(int h = 0; h < counts[k]; h++) {
//取出元素
data[index] = temp[k][h];
//记录下一个位置
index++;
}
//把数量置为0
counts[k] = 0;
}
}
}
}
}
如果此博客对你学习八大排序算法,有一点小帮助,点个赞吧!啊哈!!!!!!