目录
冒泡排序是比较基础的排序算法之一,其思想是相邻的元素两两比较,较大的数下沉,较小的数冒起来,这样一趟比较下来,最大(小)值就会排列在一端。整个过程如同气泡冒起,因此被称作冒泡排序。
冒泡排序的步骤是比较固定的:
1>比较相邻的元素。如果第一个比第二个大,就交换他们两个。 2>每趟从第一对相邻元素开始,对每一对相邻元素作同样的工作,直到最后一对。 3>针对所有的元素重复以上的步骤,除了已排序过的元素(每趟排序后的最后一个元素),直到没有任何一对数字需要比较。
- public static void bubbleSort(int[] arr) {
- if(arr==null||arr.length<2)
- return;
- for(int e = arr.length-1; e > 0; e--){//控制比较的轮数,一共n-1轮
- for(int i = 0; i < e; i++){//3
- if(arr[i]>arr[i+1])
- swap(arr,i,i+1);
- }
- }
- }
冒泡排序优化思想:双向遍历
上面的两种优化都是单向遍历比较的,然而在很多时候,遍历的过程可以从两端进行,从而提升效率。因此在冒泡排序中,其实也可以进行双向循环,正向循环把最大元素移动到数组末尾,逆向循环把最小元素移动到数组首部。该种排序方式也叫双向冒泡排序,也叫鸡尾酒排序。
- static void bubbleSort(int[] array) {
- int arrayLength = array.length;
-
- int preIndex = 0;
- int backIndex = arrayLength - 1;
- while(preIndex < backIndex) {
- preSort(array, arrayLength, preIndex);
- preIndex++;
-
- if (preIndex >= backIndex) {
- break;
- }
-
- backSort(array, backIndex);
- backIndex--;
- }
- }
-
- // 从前向后排序
- static void preSort(int[] array, int length, int preIndex) {
- for (int i = preIndex + 1; i < length; i++) {
- if (array[preIndex] > array[i]) {
- swap(array, preIndex, i);
- }
- }
- }
-
- // 从后向前排序
- static void backSort(int[] array, int backIndex) {
- for (int i = backIndex - 1; i >= 0; i--) {
- if (array[i] > array[backIndex]) {
- swap(array, i, backIndex);
- }
- }
- }
-
- static void swap (int[] arr, int i, int j) {
- int temp = arr[i];
- arr[i] = arr[j];
- arr[j] = temp;
- }
-
选择排序是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,继续放在起始位置知道未排序元素个数为0。
选择排序的步骤:
1>首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。 2>再从剩余未排序元素中继续寻找最小(大)元素,然后放到未排序序列的起始位置。 3>重复第二步,直到所有元素均排序完毕。
- public static void selectionSort(int[] arr) {
- if(arr==null||arr.length<2)
- return;
- for(int i=0;i
-1;i++) { - int minIndex = i;
- for (int j = i+1; j < arr.length; j++) {
- minIndex = arr[i] > arr[j] ? j : minIndex;
- }
- swap(arr, i, minIndex);
- }
- }
选择排序优化思想:
选择排序的优化思路一般是在一趟遍历中,同时找出最大值与最小值,放到数组两端,这样就能将遍历的趟数减少一半。第一次选择最大值与最小值
插入排序也是一种常见的排序算法,插入排序的思想是:将初始数据分为有序部分和无序部分,每一步将一个无序部分的数据插入到前面已经排好序的有序部分中,直到插完所有元素为止。 插入排序的步骤如下:每次从无序部分中取出一个元素,与有序部分中的元素从后向前依次进行比较,并找到合适的位置,将该元素插到有序组当中。
- public static void insertionSort(int[] arr){
- if (arr == null || arr.length< 2){
- return;
- //e~e有序的
- //0~i想有序
- for(int i=1;i‹arr.length;i++){// 0~i做到有序
- for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]){
- //外层遍历到i位置,此时用i和i-1两位置进行比较,直到i前面的数都有序或者到位置为0
- swap(arr, j, j + 1);
- }
- }
- }
插入排序优化思想:折半插入排序
该类优化有二分的思想,是在将待排序的元素与有序部分的元素比较时,不再挨个比较,而是用二分折中的方式进行比较,加快比较效率。
1)整体就是一个简单递归,左边排好序、右边排好序、让其整体有序
2)让其整体有序的过程里用了排外序方法
3)利用master公式来求解时间复杂度
4)归并排序的实质 时间复杂度0(N*IogN),额外空间复杂度0(N)
- public static void process(int[] arr, int L, int R) {
- if(L ==R){
- return;
- }
- int mid = L + ((R - L) >> 1);
- process(arr, L, mid);
- process(arr, mid + 1, R);
- merge(arr, L, mid, R);
- }
- public static void merge(int[] arr, int L, int M, int R) {
- int[] help = new int[R - L + 1];
- int i = 0;
- int p1 = L;
- int p2 = M + 1;
- while (p1 <= M && p2 <= R) {
- help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
- }
- while (p1 <= M) {
- help[i++] = arr[p1++];
- }
- while (p2 <= R) {
- help[i++]= arr[p2++];
- }
- for (i = 0; i < help.length; i++) {
- arr[L + i] = help[i];//将临时数组的值赋值给原数组,注意是从L开始的
- }
- }
给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量
输入:nums = [5,2,6,1] 输出:[2,1,1,0] 解释: 5 的右侧有 2 个更小的元素 (2 和 1) 2 的右侧仅有 1 个更小的元素 (1) 6 的右侧有 1 个更小的元素 (1) 1 的右侧有 0 个更小的元素
利用归并排序简化思想: 每合并一次就进行一次小于的比较,使得遍历一次就能拿到所有小于当前元素的个数(和)
下面代码是计算左侧小于当前元素的和
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例子:[1,3,4,2,5]1左边比1小的数,没有;
3左边比3小的数,1;
4左边比4小的数,1、3;
2左边比2小的数,1;
5左边比5小的数,1、3、4、2;
所以小和为1+1+3+1+1+3+4+2=16
- public static int process(int[] arr, int i, int r) {
- if(i==r){
- return 0;
- }
- int mid = i +((r -i)>> 1);
- return process(arr, i, mid) + process(arr, mid + 1, r) + merge(arr, i, mid, r);
- }
- public static int merge(int[] arr, int L, int m, int r) {
- int[] help = new int[r -L + 1];//开辟空间
- int i =0;
- int p1 = L;
- int p2=m+ 1;
- int res = 0;
- while (p1 <= m && p2 <= r){
- res+=arr[p1]< arr[p2]?(r-p2+1)*arr[p1]:0;//只要该值大于左边数组,那么该值右边的一定大于
- help[i++] = anr[p1] < arr[p2] ? arr[pl++]: arr[p2++];//当两值相等的时候必须先插入右边的,否则无法计算右边数组中到底有多少个大于该数的
- }
- while (p1<= m){
- help[i++] = arr[pl++];
- }
- while (p2 <= r) {
- help[i++] =arr[p2++];
- }
- for (i = 0;i < help.length;i++) {
- arr[L + i] = help[i];//会放到arr数组当中,方便下次递归使用
- }
- return res;
- }
问题一
快排1.0版本,一次排一个数
给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度0(1),时间复杂度0(N)
解题思路: 1)[i] <= num, [i] 与 <=区 的下一个数交换,<=区右扩,i++ 2)[i] > num, i++
左侧为 <=区 ,右侧为待定区
问题二(荷兰国旗问题)
快排2.0版本,一次排一堆相同的数
给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度0(1),时间复杂度O(N)
解题思路: 1)[i] < num, [i]和<区下一个交换,<区右移,i++ 2) [i] == num, i++ 3) [i] > num, [i]和>区前一个交换,>区右移,i原地不动(因为交换后还未进行比较)
快速排序也是一种较为基础的排序算法,其效率比冒泡排序算法有大幅提升。因为使用冒泡排序时,一趟只能选出一个最值,有n个元素最多就要执行n - 1趟比较。而使用快速排序时,一次可以将所有元素按大小分成两堆,也就是平均情况下需要logn轮就可以完成排序。
快速排序的思想是:每趟排序时选出一个基准值,然后将所有元素与该基准值比较,并按大小分成左右两堆,然后递归执行该过程,直到所有元素都完成排序。
快速排序的步骤如下:
1)先从数列中取出一个数作为基准数。
2)分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3)再对左右区间重复第二步,直到各区间只有一个数。
在选择基准值的时候,越靠近中间,性能越好;越靠近两边,性能越差。3.0版本 随机选一个数进行划分的目的就是让好情况和差情况都变成概率事件。把每一种情况都列出来,会有每种情况下的时间复杂度,但概率都是1/N。那么所有情况都考虑,时间复杂度就是这种概率模型下的长期期望。
时间复杂度O(N*logN),额外空间复杂度O(logN)都是这么来的。
- public static void quickSort(int[] arr) {
- if (arr == null || arr.length <2){
- return;
- }
- quickSort(arr, 0, arr.length - 1);
- }
-
- // arr[1..r]排好序
- public static void quickSort(int[] arr, int L, int R) {
- if(L< R){
- swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
- int[] p = partition(arr, L, R);
- quickSort(arr, L, p[0]-1); // < 区
- quickSort(arr, p[1]+1, R); // > 区
- }
- }
- //这是一个处理arr[1..r]的函数
- //默认以arr[r]做划分,arr[r]->p
p
- //返回等于区域(左边界,右边界),所以返回一个长度为2的数组res,res[o]res[1]
- public static int[] partition(int[] arr, int L, int R) {
- int less=L-1;//<区右边界
- int more=R;//>区左边界
- while(L< more){ // L表示当前数的位置 arr[R] -> 划分值,当当前位置大于等于>区左边界时跳出
- if(arr[L]
//当前数‹划分值 - swap(arr, ++less, L++);
- }else if(arr[L]>arr[R]){//当前数>划分值
- swap(arr, --more, L);
- } else {
- L++;
- }
- }
- swap(arr, more, R);
- return new int[] { less + 1, more };
- }
左儿子:2 * i + 1
右儿子:2 * i + 1
父:(i - 1)/ 2
父节点的数比子节点的数要大,示例:
利用新进堆的数与父节点比较,形成大根堆,把新的数插入到堆中,就是上移:
- //取出最大元素之后执行此操作变成大根堆
- public static void heapInsert(int[] arr, int index) {
- while (arr[index] > arr[(index - 1) / 2]) {//当前节点数值大于父节点位置
- swap(arr, index, (index - 1) /2);
- index = (index - 1)/2 ;
- }
- }
某数a在index位置,将其往下移动,至堆结构符合大根堆要求,就是下移:
- //大根堆什么也没有,此时不断进行插入排序,执行此操作
-
- //某数a在index位置,将其往下移动
- public static void heapify(int[] arr, int index, int size) {//size为数组长度
- int left = index * 2 + 1;//左孩子位置
- while (left < size) {//判断孩子是否存在
- //只有当右孩子存在且大于左孩子时,才取右孩子作为最大值;
- //其余情况选左孩子,包括
- // 1.右孩子不存在
- // 2.右孩子存在但没左孩子大
- //largest记录最大值的位置
- int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
- //比较父节点和大孩子之间谁大,记录下大的值的位置
- largest = arr[largest] > arr[index] ? largest : index;
- //如果父节点比较大,堆结构排好,退出
- if (largest == index) {
- break;
- }
- //孩子比较大,交换父和孩子的位置
- swap(arr, largest, index);
- //记录某数a的新位置
- index = largest;
- //记录处于新位置的某数a的左孩子
- left = index * 2 + 1;
- }
- }
新增一个数,或删除最大值,调整的复杂度都是 O(logN)。
做法2 O(NlogN):
所有数字先入大根堆,然后将最大数字于heapsize最后一个元素交换,heapsize减一,然后第一个数做heapify的下移操作,如此反复,就能将全部数字排序,调整的复杂度都是 O(NlogN)。
- public static void heapSort(int[] arr) {
- if (arr == null || arr.length < 2) {
- return;
- }
- //将所有数字搞成大根堆
- //做法1:
- // for (int i = 0; i < arr.length; i++) {// O(N)
- // heapInsert(arr, i);// O(logN)
- // }
- //做法2:
- for (int i = arr.length-1; i >= 0 ; i--) {
- heapify(arr, i, arr.length);
- }
- int size = arr.length;
- //0位置上的数与heapsize最后一个数交换
- swap(arr, 0, --size);
- while (size > 0) {// O(N)
- //0位置上的数重新调整位置
- heapify(arr, 0, size);// O(logN)
- //0位置上的数与heapsize最后一个数交换,heapsize减小
- swap(arr, 0, --size);// O(1)
- }
- }
-
做法1 O(N):
如果只是进行大根堆排序有O(N)的算法,即算法一:
第一步,全部数字变成大根堆,有优化做法,最小的树做heapify,然后次小…
时间复杂度分析:
假设一共N个数 最底层最差情况是heapInsert一次,共N/2个数需要进行heapInsert, 倒数第二层最差情况是heapInsert二次,共N/4个数需要进行heapInsert,如此类推:
复杂度使用错位相加法:
最终复杂度为 O(N)
黑盒封装的优先队列实际上就是小根堆
- public static void main(String[] args) {
- PriorityQueue
heap = new PriorityQueue<>(); - heap.add(8);
- heap.add(3);
- heap.add(6);
- heap.add(2);
- heap.add(4);
- while (!heap.isEmpty()){
- System.out.println(heap.poll());
- }
- }
- //输出:2 3 4 6 8
小根堆会遇到不够空间时扩容,扩容就会复制一次(2,4,8,16,32),长度为多少,复杂度就为多少,一共扩容 logN 次,总扩容复杂度为 O(N*logN),均摊下来每个元素,复杂度为O(logN)。
如果给其传入比较器可以变成大根堆;
比较器的使用
1)比较器的实质就是重载比较运算符
2)比较器可以很好的应用在特殊标准的排序上
3)比较器可以很好的应用在根据特殊标准排序的结构上
- public static class AComp implements Comparator<Integer>{
- //如果返回负数,认为第一个参数应该排在前面
- //如果返回正数,认为第二个参数应该排在前面
- //如果返回0,认为谁放前面都行
- @Override
- public int compare(Integer argo, Integer arg1) {
- return arg1 - arg0;
- }
- }
-
- public static void main(String[] args){
- PriorityQueue
heap = new PriorityQueue<>(new AComp()); - heap.add(6);
- heap.add(9);
- heap.add(3);
- heap.add(2);
- heap.add(10);
- while(!heap.isEmpty()) {
- System.out.println(heap.pol1());
- }
- }
已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
因为0位置上的正确数一定在0-6这七个数中,所以将这7个数在小根堆中排好序,最小值就可以弹出放到0位置上,然后再加入下一个数,进行重复操作。复杂度为O(Nlogk)。
- public void sortedArrDistanceLessK(int[] arr, int k) {
- PriorityQueue
heap = new PriorityQueue<>(); - int index = 0;
- //k个数形成小根堆
- for (; index < Math.min(arr.length, k); index++) {
- //index < Math.min(arr.length, k)进行的操作是避免传的参数不合适造成程序的无法正常运行
- heap.add(arr[index]);
- }
- int i = 0;
- for (; index < arr.length; i++, index++) {
- heap.add(arr[index]);//加一个数
- arr[i] = heap.poll();//弹出一个最小值
- }
- while (!heap.isEmpty()) {//依次弹出k个最小值
- arr[i++] = heap.poll();
- }
- }
-
系统提供的堆,只能给一个数,弹出一个数,不能做到上述的高效操作,要实现有高效操作的,必须自己写。
桶排序思想下的排序
1)计数排序
2)基数排序
分析:
1)桶排序思想下的排序都是不基于比较的排序
2)时间复杂度为0(N),额外空间负载度O(M)
3)应用范围有限,需要样本的数据状况满足桶的划分
先按个位数放进桶,然后从左往右,先进先出导出,再按十位数排序,重复,再按百位
代码的实现count不是记录桶 i 里面有多少个数,而是记录 ≤ i 里面有多少个数。
- // only for no-negative value
- public static void radixSort(int[] arr) {
- if (arr == null || arr.length < 2) {
- return;
- }
- radixSort(arr, 0, arr.length - 1, maxbits(arr));
- }
- //计算最大的十进制位是第几位
- public static int maxbits(int[] arr) {
- int max = Integer.MIN_VALUE;
- for (int i = 0; i < arr.length; i++) {
- max = Math.max(max, arr[i]);//寻找数组中最大的数
- }
- int res = 0;
- while (max != 0) {
- res++;
- max /= 10;//自动整除,因为max是int
- }
- return res;
- }
-
- public static void radixSort(int[] arr, int begin, int end, int digit) {
- final int radix = 10;
- int i = 0, j = 0;
-
- int[] bucket = new int[end - begin + 1];
- //digit多少哥十进制位,也代表入桶出桶的次数
- for (int d = 1; d <= digit; d++) {
- int[] count = new int[radix];
- //用于记录当前位上等于0,...,等于9的各有多少个数
- for (i = begin; i <= end; i++) {
- j = getDigit(arr[i], d);//确认当位上的数是多少
- count[j]++;//等于该位上的数,统计加1
- }
- //用于记录当前位上小于等于0,...,小于等于9的各有多少个数
- //同时也记录了当前位上等于0,...,等于9的数组最后一个数出桶后的位置
- for (i = 1; i < radix; i++) {
- count[i] = count[i] + count[i - 1];
- }
- for (i = end; i >= begin; i--) {
- j = getDigit(arr[i], d);
- bucket[count[j] - 1] = arr[i];//出桶后的位置上放该数
- count[j]--;//该桶上的数减一
- }
- for (i = begin, j = 0; i <= end; i++, j++) {
- //把bucket的数组导入arr中,相当于保留了这次桶排序
- arr[i] = bucket[j];
- }
- }
- }
同样值的个体之间,如果不因为排序而改变相对次序,就是这个排序是有稳定性的;否则就没有。
误区:认为稳定性是随数据的差异会影响算法的时间空间复杂度【容易产生的认知】
不具备稳定性的排序: 选择排序、快速排序、堆排序
具备稳定性的排序: 冒泡排序、插入排序、归并排序、一切桶排序思想下的排序
总结:只要有跨度的交换,就会丧失稳定性。相邻交换的则不会。
不具有稳定性的排序算法:
选择排序
快速排序
堆排序更是无视稳定性,他只认识孩子和父亲,进行大跨度无序交换
具有稳定性的排序算法:
冒泡排序
插入排序
归并排序关键在于merge的时候,要先拷贝左边的数,而用归并解决小和问题的时候,要先拷贝右边的数,则丧失稳定性
目前没有找到时间复杂度0(N*logN),额外空间复杂度0(1),又稳定的排序。
归并排序的额外空间复杂度可以变成0(1),但是非常难,不需要掌握,有兴趣可以搜“归并排序内部缓存法”
“原地归并排序”的帖子都是垃圾,会让归并排序的时间复杂度变成O(N~2)
快速排序可以做到稳定性问题,但是非常难,不需要掌握,可以搜“01stable sort"
所有的改进都不重要,因为目前没有找到时间复杂度0(N*logN),额外空间复杂度0(1),又稳定的排序。
有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变,碰到这个问题,可以怼面试官。
1)充分利用O(N*IogN)和0(N^2)排序各自的优势 2)稳定性的考虑
在快速排序中,当样本量小于60的时候,插入排序时间复杂度相当,当常数操作复杂度极低,因此可以将2种混合起来。
- public class Test {
- public static void quickSort(int[] arr, int 1, int r) {
- if(1 ==r){
- return;
- }
- if (1 >r - 60) {//插入排序
- 在arr[1..r]插入排序
- O(Nへ2)小样本量的时候,跑的快
- return;
- }
- swap(arr, 1 + (int) (Math.random() * (r - 1 + 1)), r);//快速排序
- int[] p = partition(arr, 1, r);
- quickSort(arr, 1, p[0] - 1); //‹ 区
- quickSort(arr, p[1] + 1, r); // > 区
- }
- }
大样本调度快选择 O(N*IogN);小样本选择插入,跑得快 0(N^2)