评判排序算法好坏的标准,之后算法性能评判的都在此基础之上进行:
in-place
和out-place
**。in-place
,原地排序,指空间复杂度为O(1)的排序算法,只占用常数内存;out-place
,非原地排序,算法需要占用额外内存。冒泡、选择、插入、归并、快速、希尔、堆排序,都是基于比较的排序,平均时间复杂度最低是O(nlogn)。
计数排序、桶排序、基数排序,都不是基于比较的排序,它们是典型的用空间换时间,在某些时候,平均时间复杂度可以比O(nlogn)更低。
从待排序序列的第一个元素开始,依次比较相邻元素的值,发现不满足大小关系则交换,将值较大的元素逐渐从前向后移动。每一次冒泡会让至少一个元素移动到它应该在的位置上。
coding:
public int[] bubbleSort(int[] arr){
for (int i = 0; i < arr.length; i++) {
//相临元素的比较
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
return arr;
}
优化:如果某次排序中没有发生交换,则可以结束排序
public int[] bubbleSort(int[] arr){
for (int i = 0; i < arr.length; i++) {
boolean flag = true;
//相临元素的比较
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = false;//说明要排序
}
}
if(flag) return arr;//说明此时数据已经有序 直接返回即可
}
return arr;
}
1.时间复杂度
最好情况下数据已经有序,只需要进行一次冒泡操作,就可以结束。冒泡排序最好的时间复杂度是O(n)。而最坏的情况就是所有数据刚好倒序与目的顺序,就需要进行n次冒泡排序,最坏的时间复杂度为O(n*n)。
2.空间复杂度
过程中只涉及相邻数据的交换,只需要常量级的临时空间,空间复杂度为O(1),是in-place排序算法。
3.是否为稳定排序算法
冒泡排序在相邻数据相同时我们不做交换,则属于稳定排序算法。(可修改)
快速排序简称快排,是对冒泡排序的一种改进,利用分治思想。基本思想是:选择一个数作为 基准点(我选的最后一个数),通过一趟排序将要排序的数据分割成独立的三部分,其中 左部分 的所有数据都比 基准点 的数据小,基准点的数据比 右部分 的所有数据都要小,然后再按此方法对这左右两部分数据分别进行快速排序,直到左右数据长度为1
时结束。整个排序过程可以递归进行,以此达到整个数据变成有序序列。
coding:
public int[] quickSort(int[] arr, int start, int end) {
if (start < end) {//这是必须一开始就判断是的点!!!
int left = start, right = end, datum = arr[start];
//实现
while (left < right) {
//右边数大于或等于基准点 指针左移一
while (left < right && arr[right] >= datum) right--;
//右边数组小于基准点 放在左边去
arr[left] = arr[right];
left++;
//左边数小于基准点 指针右移一
while (left < right && arr[left] < datum) left++;
//左边数大于或等于基准点 放到右边
arr[right] = arr[left];
right--;
}
//一趟循环结束 此时left=right 将基准点数据载入
arr[left] = datum;
//分治递归
quickSort(arr, start, left - 1);
quickSort(arr, left + 1, end);
}
return arr;
}
1.时间复杂度
最好时间复杂和平均时间复杂度为O(nlogn),极端情况下为O(n^2)
2.空间复杂度
不需要额外空间,所以空间复杂度是O(n),in-place原地排序算法。
3.是否为稳定排序算法
分区后,我们使用的策略是如果两个数据相同也可能会会交换,所以快速排序不是一个稳定的排序算法。
分为已排序区间
和未排序区间
。已排序区间初始为空。每次排序会从 未排序区间 找到最小的元素,将其放到 已排序区间 末尾,实现升序,降序反之。
基本思想:
第一次从 arr[0]~arr[n-1]中选取最小值,与 arr[0]交换
第二次从 arr[1]~arr[n-1]中选取最小值,与 arr[1]交换
第三次从 arr[2]~arr[n-1]中选取最小值,与 arr[2] 交换
第 i 次从 arr[i-1]~arr[n-1]中选取最小值,与 arr[i-1]交换
第 n-1 次从 arr[n-2]~arr[n-1]中选取最小值,与 arr[n-2]交换
总共通过 n-1 次,得到一个按排序码从小到大排列的有序序列。coding:
public int[] selectSort(int[] arr){
for (int i = 0; i < arr.length; i++) {
int minIndex = i;
//从未排序区间的序列中找到最小值
for (int j = i; j < arr.length; j++) {
if(arr[minIndex] > arr[j]) minIndex = j;
}
//有序序列尾部追加最小值 交换数据
int cur = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = cur;
}
return arr;
}
1.时间复杂度
结合之前的分析方法,最好的时间复杂度为O(n^2),最坏的时间复杂度为O(n^2),平均时间复杂度为O(n^2)。
2.空间复杂度
空间复杂度是O(1),in-place原地排序算法。
3.是否为稳定排序算法
把数据分为前后两部分,对前后两部分分别排序,再将排好序的两部分合并在一起。使用到分治思想(后面讲到),一句话概括就是大事化为小事来解决,一般使用递归来实现。分治是一种解决的问题的处理思想,递归是一种编程技巧,两者不冲突。需要写出归并排序的递推公式:
mergeSort(m->n) = mergeSort(m->k) + mergeSort(k+1->n);当m=n时终止
动图:
核心思想:
其中合并出:
来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤
左边序列和右边序列分别有一个索引指向第一个元素,然后进行比较,较小的元素存入一个临时数组temp,较小元素边序列索引右移,以此往复不断比较存入,直到一边的索引走到该子序列的最后;然后将有剩余数据的序列的剩余值直接按序存入temp数组中;
最后所有元素都存入临时数组temp中,此时temp数组中的元素有序;
最后将temp数组拷贝回原数组,即实现了排序。
coding:
//写代码的时候先写合并操作merge() 再写分解操作
public int[] mergeSort(int[] arr){
if(arr.length < 2) return arr;
//拆为两份 一份最少为1个数据
int mid = arr.length/2;
int[] left = Arrays.copyOfRange(arr,0,mid);//使用Arrays.copyOfRange()复制数组
int[] right = Arrays.copyOfRange(arr,mid,arr.length);
//分解+合并
return merge(mergeSort(left),mergeSort(right));
}
//合并 双指针很巧妙
private int[] merge(int[] left ,int[] right){
//新数组
int[] newArr = new int[left.length + right.length];
int l = 0,r = 0;//分别代表left和right数组的指针
for (int i = 0; i < newArr.length; i++) {
if(l >= left.length) newArr[i] = right[r++];
else if(r >= right.length) newArr[i] = left[l++];
else if(left[l] < right[r]) newArr[i] = left[l++];//升序
else newArr[i] = right[r++];
}
return newArr;
}
1.时间复杂度
2.空间复杂度
空间复杂度不会像时间复杂度那样类加,在合并结束后临时开辟的内存空间会被释放掉,在任意时刻CPU只有一个函数在执行,临时空间内存大小不会超过n,所以空间复杂度是O(n),out-place非原地排序算法。
3.是否为稳定排序算法
merge()
合并函数,我们使用的策略是如果两个数据相同就保存次序不变,所以归并排序是一个稳定的排序算法。
将数组中的元n素分为一个已排序区间
和一个未排序区间
,开始时 已排序区间 中只包含一个元素,未排序区间中包含有 n-1 个元素,排序过程中每次从 未排序区间 中取出第一个元素,与 已排序区间 中的元素进行比较,将它插入到 已排序区间 中的适当位置,使之成为新的 已排序区间,直到 未排序区间 的元素为空。
coding:
public int[] insertSort(int[] arr){
//从1下标开始,默认0下标为初始排序区间
for(int i=1;i<arr.length;i++){
//记录当前指针的值 preIndex记录前一个指针
int cur = arr[i],int preIndex = i - 1;
while(preIndex >= 0 && arr[preIndex] > cur){
arr[preIndex+1] = arr[preIndex];//移位
preIndex--;//更新指针
}
//把cur插入有序区间
arr[preIndex+1] = cur;
}
return arr;
}
1.时间复杂度
如果数据有序,不需要搬移数据,每次只需要比较一个数据就能确定插入位置,最好的时间复杂度为O(n);如果数组倒序,每次插入相当于在有序区间的第一个位置插入,就需要移动大量的数据,最坏的时间复杂度为O(n2)**。每次都向数组中插入一个数据,循环n次操作,需要移动n/2次,**平均时间复杂度为O(n2)。
2.空间复杂度
插入排序不需要额外的存储空间,空间复杂度是O(1),in-place原地排序算法。
3.是否为稳定排序算法
我们可以选择将后面出现的元素插入到前面出现的元素后面,插入排序是稳定排序算法。
也是一种插入排序。简单的插入排序可能存在的问题:当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响。为了改进,提出了希尔排序算法。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序的基本步骤:在此选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处做示例使用希尔增量。
coding:
//希尔排序
public int[] shellSort(int[] arr) {
// 间隔为inc增量间隔为len/2
for (int inc = arr.length / 2; inc > 0; inc /= 2) {
// 每一趟采用插入排序 等价于上面的插入排序
for (int i = inc; i < arr.length; i+=inc) {
int cur = arr[i],index = i - inc;//index指向前一个 即已排序末端
while (index >= 0 && cur < arr[index]){
arr[index+inc] = arr[index];
index-= inc;
}
arr[index + inc] = cur;
}
}
return arr;
}
1. 时间复杂度
最坏情况下,每两个数都要比较并交换一次,则最坏情况下的时间复杂度为O(n2), 最好情况下,数组是有序的,不需要交换,只需要比较,则最好情况下的时间复杂度为O(n)。
经大量人研究,希尔排序的平均时间复杂度为O(n1.3)(这个我也不知道咋来的,书上和博客上都这样说,也没找到个具体的依据)。
2. 空间复杂度
希尔排序,只需要一个变量用于两数交换,与n的大小无关,所以空间复杂度为:O(1)。
3.是否为稳定排序算法
希尔排序不是稳定的排序算法。
统计每个整数在数组中出现的次数,进而推导出每个整数在有序数组中的索引。看一个例子
假设array中的最小值为min,最大值为max:
比如元素8在有序数组中的索引为bucket[8–3]–1,结果为7。
倒数第1个元素7在有序序列中的索引bucket[7-3]–1,结果为6。
倒数第2个元素7在有序序列中的索引bucket[7–3]–2,结果为5。
coding:
//计数排序
public int[] countSort(int[] arr){
//1.求出待排序数据中最大值和最小值
int max = arr[0],min = arr[0];
for (int i = 0; i < arr.length; i++) {
if(arr[i] > max) max = arr[i];
if(arr[i] < min) min = arr[i];
}
//2.定义一个额外的数组 记录数据存在的个数
int[] bucket = new int[(max - min) + 1];
//3.统计元素的个数
for (int i = 0; i < arr.length; i++) {
int bucketIndex = arr[i] - min;
bucket[bucketIndex]++;//int[]数组默认值为0
}
//4.对数组中的数据进行累加操作
for (int i = 1; i < bucket.length; i++) bucket[i] += bucket[i-1];
//5.创建一个临时数组 存储最终有序的序列
int[] tmp = new int[arr.length];
//6.逆序扫描待排序数列 保证算法稳定性
for (int i = arr.length-1; i >= 0; i--) {
int bucketIndex = arr[i] - min;
tmp[bucket[bucketIndex] - 1] = arr[i];
bucket[bucketIndex] -= 1;//这一步保证了算法的稳定性 聪明绝顶
}
//7.将临时数据依次放入元素数组中
for (int i = 0; i < tmp.length; i++) arr[i] = tmp[i];
//8.返回arr
return arr;
}
1.时间复杂度
结合之前的分析方法,最好、最坏、平均的时间复杂度为O(n+k),k为桶的数量。
2.空间复杂度
空间复杂度是O(k),out-place原地排序算法。
3.是否为稳定排序算法
我们使用的策略是bucket[bucketIndex] -= 1;
也就是被访问依次数据的值减少一次,也就是对应的指针前移一次,这一点光看是不会明白的,需要带入数据进行理解。所以计数排序是稳定的排序算法。
桶排序面试:有100亿订单,对这100亿订单排序,就可以把数据读取到文件中,再到文件中对数据进行排序,就完美解决了这样一个问题。
桶排序是一种基于计数的排序算法(下面有提到),工作的原理是将数据分到有限数量的桶子里,然后每个桶再分别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。看下面动图:
排序动画过程解释:
coding:
//桶排序
public List<Integer> bucketSort(List<Integer> array, int bucketSize) {
//1.合法性检验
if (array == null || array.size() < 2 || bucketSize < 1) return array;
//2.找出元素中最大值和最小值
int max = array.get(0), min = array.get(0);
for (int i = 0; i < array.size(); i++) {
if (array.get(i) > max) max = array.get(i);
if (array.get(i) < min) min = array.get(i);
}
//3.计算桶个数 min~max表示桶数据的取值范围 +1是为什么
int bucketCount = (max - min) / bucketSize + 1;
//4.按照顺序创建桶 list嵌套list
List<List<Integer>> bucketList = new ArrayList<>();
for (int i = 0; i < bucketCount; i++) bucketList.add(new LinkedList<>());
//5.把待排序的集合依次添加待对应的桶中
for (int i = 0; i < array.size(); i++) {
//得到对应数据的桶的索引
int bucketIndex = (array.get(i) - min) / bucketSize;
bucketList.get(bucketIndex).add(array.get(i));
}
//6.对桶中数据排序 使用sort()方法实现
List<Integer> result = new ArrayList<>();
for (int i = 0; i < bucketList.size(); i++) {
List<Integer> list = bucketList.get(i);
list.sort((a,b) -> (a - b));//升序
for (int j = 0; j < list.size(); j++) result.add(list.get(j));
}
//7.返回result
return result;
}
基数排序是桶排序的扩展。它是将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。看动图:
思路分析:
基数排序第i
趟将待排数组里的每个数的i
位数放到对应的桶ducket
中,然后再从这十个桶中取出数据,重新放到原数组里,直到i
大于待排数的最大位数(比如i=4时就大于100的最大位数了)。
1.数组里的数最大位数是n位,就需要排n趟;
2.若数组里共有m个数,则需要十个长度为m的 ducket
(数组) 用来暂存i
位上的数,例如,第1趟,各位数为0的会被分配到下标为0的桶里,各位数为1的会被分配到下标为1的桶里;
3.分配结束后,再依次从ducket
中取出数据,遵循先进先出原则,例如对数组{1,11,2,44,4},进行第1趟分配后,ducket1
={1,11},ducket2
={2},ducket3
={44,4},依次取出元素后{1,11,2,44,4},第一趟结束
4.循环到n趟后结束,排序完成。
coding:
//基数排序
private int[] radixSort(int[] arr) {
//1.求出待排数的最大长度
int maxLength = arr[0];
for (int i = 0; i < arr.length; i++) {
if(maxLength < arr[i]) maxLength = arr[i];
}
//根据最大数求最大长度 转换为string 比如100最大长度为3
maxLength = (maxLength+"").length();
//2.用于暂存数据的数组
int[][] tmp = new int[10][arr.length];
//用于记录tmp数组中每个桶内存的数据量
int[] counts = new int[10];
//用于记录每个数的i位数
int num = 0;
//用于取的元素需要放的位置索引
int index = 0;
//3.根据最大程度决定排序的次数
for (int i = 0,n=1; i < maxLength; i++,n *= 10) {
for (int j = 0; j < arr.length; j++) {
num = (arr[j] / n) % 10;
tmp[num][counts[num]] = arr[j];
counts[num]++;
}
//4.从tmp中取元素重新放到arr数组中
for (int j = 0; j < counts.length; j++) {
for (int k = 0; k < counts[j]; k++) {
arr[index] = tmp[j][k];
index++;
}
counts[j] = 0;
}
index = 0;
}
return arr;
}
1. 时间复杂度:
每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
系数2可以省略,且无论数组是否有序,都需要从个位排到最大位数,所以时间复杂度始终为O(d*n) 。其中,n是数组长度,d是最大位数。
2. 空间复杂度:
基数排序的空间复杂度为O(n+k),其中k为桶的数量,需要分配n个数。
3. 是否为稳定排序算法
基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法。
参考:https://code.i-harness.com/zh-CN/q/e98fa9
在找最大数的同时找最小值 如果最小值小于0就给每个值加上最小值的相反数,再比较
先来了解一下堆的相关概念。堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
了解了这些定义。接下来看看堆排序的基本思想及基本步骤
一般升序采用大顶堆,降序采用小顶堆。下面是步骤图解:
步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
此时,我们就将一个无需序列构造成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
再简单总结下堆排序的基本思路:
堆排序动图:
coding:
//堆排序
public int[] heapSort(int[] arr) {
//将无序序列构建成一个堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length);
}
//将堆顶元素和末尾元素交换,将最大元素放置数组末端
//重新调整至堆结构,然后继续将堆顶元素和当前末尾元素交换,以此往复
for (int i = arr.length - 1; i > 0; i--) {
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, i);
}
return arr;
}
/**
* 将二叉树调整为堆
*
* @param arr 待调整的数组
* @param i 表示非叶子结点在数组中索引
* @param length 表示对多少个元素继续调整,length逐渐减少
*/
public void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i];
//k=2i+1是i的左子节点
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && arr[k] < arr[k + 1])//左子节点的值<右子节点的值
k++;//指向右节点
if (arr[k] > temp) {//如果子结点的值>父节点的值
arr[i] = arr[k];//将较大的值赋给当前节点
i = k;//i指向k,继续循环比较
} else
break;
}
//for循环后,已经将以i为父结点的树的最大值,放在了顶部
arr[i] = temp;//将temp值放到调整后的位置
}
1. 时间复杂度
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度最好和最坏情况下都是 O(nlogn)。
2. 空间复杂度
堆排序不要任何辅助数组,只需要一个辅助变量,所占空间是常数与n无关,所以空间复杂度为O(1)。
3. 是否为稳定排序算法
堆排序不是稳定的排序算法。