话不多说:对于排序算法,可以大致的分为两类 :
比较类排序:通过比较决定元素之间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序,相反道理,不通过比较来决定元素之间的相对次序,它可以突破基于比较排序的时间下界,以线性时间来运行,因此也称为线性时间非比较类排序
仿照网络大神做了一张思维导图:
对于算法复杂度:
在先了解其复杂度及稳定性之前,我们先了解一下什么是复杂度和稳定性:
时间复杂度和空间复杂度:
定义:一个算法中的语句执行次数称为语句频度或者时间频度;
一般来:检验算法的效率,主要考虑最坏时间复杂度和平均时间复杂度。如果没有严格说明,那就是以最坏时间复杂度为标准计量。
时间复杂度:
- 概念
- 一个算法执行所耗费的时间,从理论上讲是无法计算,明确的。但是单从具体算法执行花费时间来判断算法是不可选。那么我们可以知道一个算法执行所花费的时间和算法中语句的执行次数是成正比的,那么我们从理论上就可以知道算法执行的次数多,那么它的花费时间就多。
- 在时间频度中,n是称为问题的规模,当n不断发生变化时,时间频度T(n)也会不断变化。但是它的变化是有规律的,所以就可以用时间复杂度这个理论上的概念来描述。一般来说,算法中的基本操作重复次数的是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得n在趋近无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n) = O(f(n)),那么称O(f(n))为算法的渐进时间复杂度,那么就是我们俗称的时间复杂度。
- 计量方式
- 如果算法的执行时间不会随着问题的规模n的增长而增长,那么即使算法中存在有上千万条代码语句,但是它是执行时间也就是一个常数。此类算法时间复杂度均为O(1);
- 按照数量级递增的。常见的时间复杂度有:常数阶O(1),对数阶O(㏒₂n),线性阶O(n),线性对数阶O(n㏒₂n),平方阶O(n²),立方阶O(n³)………等,K次方阶O(nk),指数阶O(2ⁿ)。当然随着问题规模n的不断增加,那么以上的时间复杂度也会随之不断增加的,那么算法的执行效率也会随之下降。
- 最简单的计量方式:一个循环为n,一个嵌套为n的+1次方,并列时是+n,最后结果取最大值。
空间复杂度
- 一个程序的空间复杂度是指运行完一个程序所需内存的大小
- 固定部分。这部分空间的空间大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(代码空间)、数据空间·(常量、简单变量)等所占的空间。这部分属于静态空间。
- 可变空间,这部分空间的主要包括动态分配的空间,已经递归栈所需的空间等。这部分空间的大小与算法有关。
- 一个算法所需的存储空间用f(n)表示。S(n) = O(f(n)),那么其中的n为问题的规模,S(n)表示空间复杂度。
稳定性
- 只如果a排序前处于b前面,a==b,排序完后a任然处于b前面,此为稳定,反之不稳定。
排序算法/类型 | 平均时间复杂度 | 最差时间复杂度 | 最优时间复杂度 | 空间复杂度 | 稳定性 |
插入排序 | O(n²) | O(n²) | O(n) | O(1) | 稳定 |
希尔排序 | O(n¹′³) | O(n²) | O(n) | O(1) | 不稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
堆排序 | O(n㏒₂n) | O(n㏒₂n) | O(n㏒₂n) | O(1) | 不稳定 |
冒泡排序 | O(n²) | O(n²) | O(n) | O(1) | 稳定 |
快速排序 | O(n㏒₂n) | O(n²) | O(n㏒₂n) | O(n㏒₂n) | 不稳定 |
归并排序 | O(n㏒₂n) | O(n㏒₂n) | O(n㏒₂n) | O(n) | 稳定 |
| | | | | |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
桶排序 | O(n+k) | O(n²) | O(n) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
冒泡排序:
冒泡排序是一种简单的排序算法。它重复的比较两个元素的大小。如果他们的顺序不符规定,那么将交换。遍历的工作是重复的进行,直到没有需要交换的,那么这数列已经排序完成。这个算法可以引申为水中气泡大小,上浮到顶端的顺序。
- 算法描述:
- 比较相邻的元素,例如第一个比较第二个,那么将交换他们两位置;
- 对每一对相邻元素作相同的工作,从开始第一个对到最后结尾的最后一对,这样在最后的元素应该会是最大的数值;
- 针对所有的元素都重复以上的步骤,除了最后一个;
- 一直重复执行a~c步骤
代码:
/**
* 冒泡排序
* <p>Title: maopao</p>
* <p>Description: </p>
* @param intArray
*/
private static void maopao(int[] intArray) {
int length = intArray.length;
for (int i = 0; i < length - 1; i++) {
for (int j = 0; j < length - 1 - i; j++) {
if (intArray[j] > intArray[j + 1]) {
int emp = intArray[j];
intArray[j] = intArray[j + 1];
intArray[j + 1] = emp;
}
}
}
选择排序
- 选择排序是一种简单直观的排序算法。它的原理就是:通过在没有排序中的序列汇总找到最小(或最大)元素,存放到排序序列的起始位置,然后,在从剩余未排序的序列中继续寻找最小(或最大)的元素,然后放到已排序的下一下标位置。以此类推,直至所有元素排序完成。
- 算法描述:
- N个记录的直接选择配置可经过n-1趟直接选择排序得到有序的结果。具体算法描述如下:
- 初始状态:无序区为R[1…n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(1..n),该趟排序从当前无序区中-选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n]分别编程记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- N-1趟结束,数组有序化了。
- 代码:
/**
* 选择排序1
* <p>
* Title: xuanze
* </p>
* <p>
* Description:
* </p>
*
* @param intArray
*/
private static void xuanze(int[] intArray) {
for (int i = 0; i < intArray.length; i++) {
int smallJ = i;
int smallValue = intArray[smallJ];
for (int j = i; j < intArray.length; j++) {
if (intArray[j] < smallValue) {
smallJ = j;
smallValue = intArray[j];
}
}
intArray[smallJ] = intArray[i];
intArray[i] = smallValue;
}
}
- 算法分析
表现最为稳定的排序算法之一了,因为无论什么数据进去都是O(n²)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不会额外占用其他内存空间。理论上讲,选择排序可能也是平时排序应用最多一种。
插入排序
插入排序的算法描述是一种简单直观的排序算法。它的工作原理就是通过构件有序的序列,对于未排序的数据,在已排序中从后向前扫描,找到相应的位置并插入。
- 算法描述:
- 一般来说插入拍摄都是采用in-place在数组上实现的。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 拿到下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已经排序的序列元素)大于新元素,将该元素移动到下一个位置;
- 重复iii步骤,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置之后
- 重复ii~v步骤
- 代码实现
/**
* 插入排序
* <p>
* Title: charu
* </p>
* <p>
* Description:
* </p>
*
* @param intArray
*/
private static void charu(int[] intArray) {
for (int i = 1; i < intArray.length; i++) {
for (int j = i; j > 0; j--) {
if (intArray[j - 1] > intArray[j]) {
int emp = intArray[j];
intArray[j] = intArray[j - 1];
intArray[j - 1] = emp;
}else {
j = 0;
}
}
}
}
- 算法分析:
插入排序在实现上,通常采用的in-place排序(即只需要使用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。其本质类似交换排序(因为排序序列为数组,不是链表,需一一挪位),依次向前排序好的序列比较,只要出现比他大的元素就停止比较,
希尔排序
1959年Shell发明,第一个突破O(n²)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
- 具体算法描述:
- 选择一个增量序列序列t1,t2,…,tk,其中ti>ti+1,tk=1;
- 按增量序列个数k,对序列进行k趟排序;
- 每趟排序,根据对应的增量ti,将待排序序列分割成若干长度为m的子序列,分别对各子序列进行直接插入排序。仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。
- 代码实现
略
4. 算法分析
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法。
归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。将已经有序的子序列合并,得到完全有序的序列;即先使得每个子序列有序,再使得子序列之间有序。若将两个有序表合并成一个有序表,称为2-路归并。
- 算法描述:
- 把长度为n的输入序列分成两个长度为n/2的子序列
- 对这两个子序列分别采用归并排序
- 将两个排序好的子序列合并成一个最终的排序序列。
- 代码:
private static void guibing(int[] intArray) {
int[] intArrayTemp = new int[intArray.length];// 此处是为了减少递归中频繁创建int数组
guibing(intArray, 0, intArray.length - 1, intArrayTemp);
}
private static void guibing(int[] intArray, int left, int right, int[] intArrayTemp) {
if (left < right) {
int mid = (left + right) / 2;
guibing(intArray, left, mid, intArrayTemp);
guibing(intArray, mid + 1, right, intArrayTemp);
// 对该序列进行合并
hebing(intArray, left, mid, right, intArrayTemp);
}
}
private static void hebing(int[] intArray, int left, int mid, int right, int[] intArrayTemp) {
int i1 = left, i2 = mid + 1;
int t = 0;
while (i1 <= mid && i2 <= right) {
if (intArray[i1] <= intArray[i2]) {
intArrayTemp[t++] = intArray[i1++];
} else {
intArrayTemp[t++] = intArray[i2++];
}
}
while (i1 <= mid) {
intArrayTemp[t++] = intArray[i1++];
}
while (i2 <= right) {
intArrayTemp[t++] = intArray[i2++];
}
t = 0;
while (left <= right) {// 关键一步
intArray[left++] = intArrayTemp[t++];
}
}
- 算法分析,归并排序是一种稳定的排序方法(其实我个人不觉得是一定稳定的)。和选择排序一样,归并排序的性能不受需排序的数据的影响,但是表现比选择排序好,因为其时间复杂度始终是O(nlogn)的。代价就是需要开辟临时内存空间。
- 个人看法:归并排序相较选择排序来说,我个人认为在归并排序中是包含了选择排序的算法理论的。归并排序其主要是靠分治思想,通过递归实现将总序列一分为二,二分为四,四分为八…通过递归实现。然后在分割的底部其单个子序列长度为2,3,那么将对子序列实现选择排序,将对应相邻的两个子序列遍历比较其元素内容大小,然后通过临时序列接收,然后将临时序列替换子序列所有内容。最终,递归顶部实现最大的两个子序列拼接。反馈排序后的序列。
快速排序
快速排序的基本思想是:通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可以通过分别对这两部分序列继续进行排序,以达到整个序列排序。
- 算法描述:
- 快速排序使用分治法来把一个串(list)分为两个子序列。具体如下:
- 从序列中挑出一个元素。称为“基准”(通常是选取序列第一个元素,然后以他内容为标准)
- 重新排序数列,所有比基准小的放到左边,比基准大的放到右边。(相同的随意)。然后在这个分区退出后,该基准就处于序列的中间位置。这称作分区。
- 然后递归的重复i~ii操作左右序列。
- 代码:
private static void kuaiSu(int[] intArray) {
kuaiSu2(intArray, 0, intArray.length - 1);
}
public static void kuaiSu2(int[] intArray, int start, int end) {
if (start >= end) {
return;
}
int index = fenqu(intArray, start, end);
kuaiSu2(intArray, start, index - 1);
kuaiSu2(intArray, index + 1, end);
}
public static int fenqu(int[] intArray, int start, int end) {
// 固定的切分方式
int key = intArray[start];
while (start < end) {
while (intArray[end] >= key && end > start) {// 从后半部分向前扫描
end--;
}
intArray[start] = intArray[end];
while (intArray[start] <= key && end > start) {
start++;
}
intArray[end] = intArray[start];
}
intArray[end] = key;
return end;
}
堆排序
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并且同时满足堆积的性质:即子节点的键值或者索引总数小于(或者大于)它的父节点。
- 算法描述
- 将初始待排序的序列构建成为大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,…Rn-1)和新的有序区(Rn),且满足R[1,2,…n-1]<=R[n];
- 由于交换后新的堆顶可能违反堆的性质,因此需要对当前无序区(R1,R2,…Rn-1)调整为新堆,然后再次将R[1]
与无序区最后一个元素交换,得到新的无序区(R1,R2…Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
- 代码实现:完全二叉树有个特性:左边子节点位置 = 当前父节点的两倍 + 1,右边子节点位置 = 当前父节点的两倍 + 2。
- 代码实现:
public class DuiPaiXu2 {
private static void daDingDui(int[] arrays, int start, int end) {
// 父节点
int curr = start;
// 左右节点
int left = curr * 2 + 1;
// int right = curr * 2 + 2;
// 父节点值
int temp = arrays[curr];
for (; left < end; curr = left, left = curr * 2 + 1) {// 将其左子节点变为父节点,将根据其变换后的父节点获取到左子节点
if (left < end && arrays[left] < arrays[left + 1]) {// 判断左右子节点谁最大
left++;// 变为右子节点为父节点,
}
if (arrays[left] > temp) {// 判断左右子节点的最大值与当前父节点的值谁大
arrays[curr] = arrays[left];
arrays[left] = temp;
}
}
}
private static void jiaoHuan(int[] arrays, int index1, int index2) {
int temp = arrays[index1];
arrays[index1] = arrays[index2];
arrays[index2] = temp;
}
private static void duiPaiXu(int[] arrays) {
int length = arrays.length;
int left;
// 从(n/2-1)--->0遍历,逐渐将待排序序列生成一个大顶堆
for (left = length / 2 - 1; left > 0; left--) {
daDingDui(arrays, left, length - 1);
}
// 从最后一个元素开始对序列进行调整,不断的缩小调整范围,直到缩小到只含有第一个元素
for (left = length - 1; left > 0; left--) {
// 交换第一个和左子节点元素后,左子叶节点就是序列中最大的元素。
jiaoHuan(arrays, 0, left);
// 调整剩下的堆序列,保证右子节点为剩下的堆序列中的最大值
daDingDui(arrays, 0, left - 1);
}
}
public static void main(String[] args) {
int arrays[] = { 20, 30, 90, 40, 70, 110, 60, 10, 100, 50, 80 };
System.out.println(Arrays.toString(arrays));
duiPaiXu(arrays);
System.out.println(Arrays.toString(arrays));
}
}
计数排序
计数排序不是基于比较的排序算法,其核心是将输入的数据值转化为键存储在额外开辟的数组空间中,作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是由确定范围的整数。
- 算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从数组C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每次放一个元素就将C(i)减去1。
- 代码:
private static void jiShu(int[] arrays) {
int min = arrays[0], max = arrays[0];
for (int i = 0; i < arrays.length; i++) {
min = min > arrays[i] ? arrays[i] : min;
max = max < arrays[i] ? arrays[i] : max;
}
int[] jiShuArrays = new int[max + 1];
for (int i = 0; i < arrays.length; i++) {
int value = arrays[i];
jiShuArrays[value] += 1;
}
for (int i = 0, a = 0; i < jiShuArrays.length; i++) {
if (jiShuArrays[i] > 0) {
arrays[a++] = i;
}
}
}
- 算法分析
计数排序是一个稳定的排序算法。当输入的元素是n个0到k之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是恨到且序列比较集中时,计数排序是一个很有效的排序算法
桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序的工作原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里面,每个桶再分别排序(有可能再次使用到排序排序算法或者是以递归的方式继续使用桶排序进行排序)。
- 算法描述
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里面去;
- 对每个不是空的桶进行排序。
- 从不是空的桶里面把排序好的数据进行拼接;
- 代码:
private static void tongPaiXu(int[] arrays) {
int min = arrays[0], max = arrays[0];
for (int i = 0; i < arrays.length; i++) {
min = min > arrays[i] ? arrays[i] : min;
max = max < arrays[i] ? arrays[i] : max;
}
int[][] arrayss = new int[max / 10 + 1][];
for (int i = 0; i < arrays.length; i++) {
int j = arrays[i] / 10;
if (arrayss[j] == null) {
int[] temp = new int[1];
temp[0] = arrays[i];
arrayss[j] = temp;
} else {
int length = arrayss[j].length;
int[] temp = new int[length + 1];
zhuanyiShuZu(arrayss[j], temp);
temp[length] = arrays[i];
arrayss[j] = temp;
}
}
for (int i = 0, a = 0; i < arrayss.length; i++) {
if (arrayss[i] != null) {
if (arrayss[i].length == 1) {
arrays[a++] = arrayss[i][0];
System.out.println(arrayss[i][0]);
} else if (arrayss[i].length > 1) {
for (int j = 0; j < arrayss[i].length; j++) {
int min2 = arrayss[i][j];
for (int j2 = j; j2 < arrayss[i].length; j2++) {
if (min2 < arrayss[i][j2]) {
int temp = arrayss[i][j2];
arrayss[i][j2] = arrayss[i][j];
arrayss[i][j] = temp;
}
}
}
for (int j = 0; j < arrayss[i].length;) {
System.out.println(arrayss[i][j]);
arrays[a++] = arrayss[i][j++];
}
}
}
}
}
private static void zhuanyiShuZu(int[] arrays1, int[] arrays2) {
System.out.println(arrays1.length + ", " + arrays2.length);
for (int i = 0; i < arrays1.length; i++) {
arrays2[i] = arrays1[i];
}
}
- 算法分析:桶排序最好的情况下就是使用线性时间O(n),桶排序的时间复杂度,取决于对各个桶之间数据进行排序的时间复杂度,因为其他部分时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少,但是相应的空间消耗就会增大。
基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再次收集;依次类推,直到最高位。有时候有些属性是有优先级顺序,先按照低优先级排序,再按照高优先级排序。最后的次序就是高优先级的在前,高优先级相同的低优先级高的在前。
- 算法描述:
- 取得数组中的最大数,并取得位数;
- Arrays为原始数组,从低位开始取到每个位组成radix数组;
- 对redix进行计数排序(利用计数排序适用于小范围数的特点)
- 代码
private static void jiShu(int[] arrays) {
// 先按照基数排序,也就是个位上数值大小
int[][] arrayss = new int[10][];
for (int i = 0; i < arrays.length; i++) {
String s = String.valueOf(arrays[i]);
int ge = Integer.parseInt(s.substring(s.length() - 1, s.length()));
if (arrayss[ge] == null) {
arrayss[ge] = new int[1];
arrayss[ge][0] = arrays[i];
} else {
int[] temp = new int[arrayss[ge].length + 1];
zhuanyiShuZu(arrayss[ge], temp);
temp[arrayss[ge].length] = arrays[i];
arrayss[ge] = temp;
}
}
// 将二维数组转换为一维数组
for (int i = 0, a = 0; i < arrayss.length; i++) {
if (arrayss[i] != null) {
for (int j = 0; j < arrayss[i].length; j++) {
arrays[a++] = arrayss[i][j];
}
}
}
// 按照最高位大小排序
arrayss = new int[10][];
for (int i = 0; i < arrays.length; i++) {
String s = String.valueOf(arrays[i]);
int gao = Integer.parseInt(s.substring(0, 1));
if (arrayss[gao] == null) {
arrayss[gao] = new int[1];
arrayss[gao][0] = arrays[i];
} else {
int[] temp = new int[arrayss[gao].length + 1];
zhuanyiShuZu(arrayss[gao], temp);
temp[arrayss[gao].length] = arrays[i];
arrayss[gao] = temp;
}
}
// 将二维数组转换为一维数组
for (int i = 0, a = 0; i < arrayss.length; i++) {
if (arrayss[i] != null) {
for (int j = 0; j < arrayss[i].length; j++) {
arrays[a++] = arrayss[i][j];
}
}
}
}
private static void zhuanyiShuZu(int[] arrays1, int[] arrays2) {
for (int i = 0; i < arrays1.length; i++) {
arrays2[i] = arrays1[i];
}
}
- 基数排序是基于分别排序,分别收集,所以是稳定的,但是基数排序的性能要比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到的新的关键字序列又需要O(n)的使劲复杂度。假如待排序数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n),当然基本上还是线性级别的。基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间还是需要大概n个。