内容:
1、复杂排序
2、排序总结
3、比较器
4、实际问题
注:实现代码为Java
1、复杂排序
说明:关于简单排序(冒泡排序、选择排序、插入排序) 看这里:https://www.cnblogs.com/wyb666/p/10106010.html
复杂排序主要是以下三种:
- 归并排序: 时间复杂度: O(N * logN) 额外空间复杂度: O(N)
- 随机快速排序: 时间复杂度: O(N * logN) 额外空间复杂度: O(logN)
- 堆排序: 时间复杂度: O(N * logN) 额外空间复杂度: O(1)
归并排序:
整体思路:利用归并的思想实现排序,采用经典的分治策略将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序
代码:
1 public static void mergeSort(int[] arr) { 2 if (arr == null || arr.length < 2) { 3 return; 4 } 5 sortProcess(arr, 0, arr.length - 1); 6 } 7 8 public static void sortProcess(int[] arr, int L, int R) { 9 if (L == R) { 10 return; 11 } 12 int mid = (L + R) / 2; // L和R中点的位置 13 sortProcess(arr, L, mid); // T(N/2) 14 sortProcess(arr, mid + 1, R); // T(N/2) 15 merge(arr, L, mid, R); // O(N) 16 // T(N) = 2*T(N/2) + O(N) => O(N * logN) 17 } 18 19 public static void merge(int[] arr, int L, int mid, int R) { 20 // merge: sort the array(L to R) 21 int[] help = new int[R - L + 1]; // auxiliary array 22 int i = 0; 23 int p1 = L; 24 int p2 = mid + 1; 25 while (p1 <= mid && p2 <= R) { 26 // if the value of arr[p1] < arr[p2] give value arr[p1] to help[i] 27 // else give value arr[p2] to help[i] 28 help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; 29 } 30 // p1p2 must be and only one arrive the boundary 31 // give the remain elements of no-arrive boundary's one(p1 or p2) to help 32 while (p1 <= mid) { 33 help[i++] = arr[p1++]; 34 } 35 while (p2 <= R) { 36 help[i++] = arr[p2++]; 37 } 38 for (i = 0; i < help.length; i++) { 39 // give elements of help to arr 40 arr[L + i] = help[i]; 41 } 42 }
随机快速排序:
快速排序:通过一轮的排序将序列分割成独立的两部分,其中一部分均比另一部分小(根据某个值划分这两个区域)。继续对长度较短的序列进行同样的分割,最后到达整体有序
众所周知,基础快速排序算法,其效率一个关键点就在与划分元素的选取,由于之前一直选取的是第一个元素,所以当遇到特殊输入,比如太大或者太小,就会造成区间划分极度不合理。
引入随机化,就是在每一次划分的时候随机选取一个元素作为关键字,用它来进行划分。由于每一次划分都是随机选取,所以每一次都选到不好的元素概率低,可以作为一个优化的方向
代码:
1 public static void quickSort(int[] arr) { 2 if (arr == null || arr.length < 2) { 3 return; 4 } 5 quickSort(arr, 0, arr.length - 1); 6 } 7 8 public static void quickSort(int[] arr, int L, int R) { 9 if (L < R) { 10 // swap使最后一个数随机 减少极限情况的影响(eg: 1 2 3 4 5这样的数组) 11 swap(arr, L + (int) (Math.random() * (R - L + 1)), R); 12 // 将小于R的数移到左边 大于R的数移到右边 然后R移到中间 返回R的范围 13 int[] p = partition(arr, L, R); 14 // 以中间区域划分 15 quickSort(arr, L, p[0] - 1); 16 quickSort(arr, p[1] + 1, R); 17 } 18 } 19 20 public static int[] partition(int[] arr, int L, int R) { 21 // 默认以最后一个位置做划分 22 int less = L - 1; 23 int more = R; 24 while (L < more) { 25 // L表示当前位置 26 if (arr[L] < arr[R]) { 27 swap(arr, L++, ++less); 28 } else if (arr[L] > arr[R]) { 29 swap(arr, L, --more); 30 } else { 31 L++; 32 } 33 } 34 35 swap(arr, more, R); // 将最后一个数替换到右边界 36 // 返回等于区域的左右边界 37 return new int[] { less + 1, more }; 38 }
堆排序:
堆排序:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列
堆排序实质上是利用了二叉树 ,实质上在工程中二叉树利用数组结构存储,抽象存储原理如下:
二叉树可以转换成数组: 节点i的左右孩子分别是2*i+1和2*i+2 节点i的父节点是(i-1)/2
大根堆和小根堆:
- 大根堆: 树的任何一个子树的最大值是这颗子树的顶部
- 小根堆: 树的任何一个子树的最小值是这颗子树的顶部
堆排序过程:
- 堆排序实际上就是利用堆结构的排序
- 首先建立一个大根堆(复杂度为O(n)) -> 使用heapInsert插入数
- 然后把大根堆第一个数和最后一个数替换 拿出这个元素把堆的size-1 然后使用heapify对堆进行调整
去掉堆顶的最大数或最小数: 把堆顶(第一个元素)和最后一个元素交换,拿出这个元素然后把堆的size-1,然后对堆顶进行hapify调整
其实优先级队列结构其实就是堆结构
代码:
1 public static void heapSort(int[] arr){ 2 if(arr==null||arr.length>2){ 3 return; 4 } 5 for(int i=0; i<arr.length; i++){ 6 heapInsert(arr, i); // 初步建立大根堆 0到i 7 } 8 int heapSize = arr.length; 9 swap(arr, 0, --heapSize); // 第一个和最后一个交换 然后heapSize-1 10 while(heapSize>0){ 11 heapify(arr, 0, --heapSize); 12 swap(arr, 0, --heapSize); 13 } 14 } 15 16 public static void heapInsert(int[] arr, int index){ 17 // 向大根堆中插入元素 并调整元素位置 18 while(arr[index]>arr[(index-1)/2]){ 19 swap(arr, index, (index-1)/2); 20 index = (index-1)/2; 21 } 22 } 23 24 public static void heapify(int[] arr, int index, int heapSize){ 25 // 拿出堆顶元素之后的调整堆 26 int left = index*2+1; 27 int right = left+1; 28 while(left<heapSize){ 29 int largest = right<heapSize && arr[right] > arr[left] ? right : left; 30 largest = arr[largest] > arr[index] ? largest : index; 31 if(largest == index){ 32 break; 33 } 34 swap(arr, largest, index); 35 index = largest; 36 left = index * 2 + 1; 37 right = left + 1; 38 } 39 }
2、排序总结
比较少用的排序(了解即可):
- 桶排序: 时间复杂度O(N) 额外空间复杂度O(N)
- 计数排序: 时间复杂度O(N) 额外空间复杂度O(N)
- 基数排序: 时间复杂度O(N) 额外空间复杂度O(N)
- 上述三种都是非基于比较的排序,与被排序的样本数据实际情况很有关系,在实际中并不常用
- 另外以上三种都是稳定的排序
关于桶排序计数排序基数排序:
1 桶: 相当于一个容器,可以是数组中的某个位置也可以是双向链表也可以是一个队列也可以是一个堆 2 把相应东西放入相应桶内 然后再从低位置的桶依次到高位置 依次把东西倒出来 3 4 eg: 数组arr长度为60 数据值为0到60 使用桶排序: 生成一个数组temp长度为61 依次遍历arr得到arr[i] 5 将temp对应的数组值++ temp[arr[i]] = temp[arr[i]] + 1 遍历完了之后遍历数组temp输出结果 6 每个下标对应几个值就把下标值输出几遍 7 实际上这个实例是计数排序 实质上是桶排序的一种实现 8 9 基数排序将计数排序进行了改进,用10个桶进行排序 分别针对个位、十位、百位
算法稳定性:
- 追求算法稳定性的意义: 第一次排序和第二次排序在某些值相等的情况下保持原来的排序顺序
- 冒泡排序: 可以实现稳定(相等的情况下后一个继续往后走)
- 插入排序: 可以实现稳定(相等就不往前插入)
- 选择排序: 做不到稳定
- 归并排序: 可以实现稳定(merge相等的时候就先移动左边的)
- 快速排序: 做不到稳定(partition时做不到)
- 堆排序: 做不到稳定
工程中的综合排序算法(例如Java内置的sort):
1 基本思想: 2 数组长度很短(大概小于60),用插入排序(插入排序常数极低) 3 数组长度很长,会先判断里面装的是什么,如果都是基本类型就用快排(不在乎稳定性),如果都是 4 自己定义的类型就用归并排序(在乎稳定性)
3、比较器
什么是比较器:
1 在使用系统的内置sort时 如果针对的是普通类型 那么会根据普通类型的值来进行排序 2 但是如果针对的不是普通类型而是自定义类型 这时候就会根据类型对应的对象在内存中的位置进行排序 3 此时排序就没有任何意义了 但是我们可以写一个自定义的比较器传入给sort使用 让sort使用我们定义的比较器来进行排序
Java中比较器的使用:
1 // 比较器返回负数表示第一个参数排序时应该放前面 2 // 比较器返回正数表示第二个参数排序时应该放前面 3 // 比较器返回0表示两个参数一样大 4 5 // 比较器 6 import java.util.*; 7 8 public class MyComparator { 9 10 public static class Student { 11 public String name; 12 public int id; 13 public int age; 14 15 public Student(String name, int id, int age) { 16 this.name = name; 17 this.id = id; 18 this.age = age; 19 } 20 } 21 22 public static class IdAscendingComparator implements Comparator<Student> { 23 // id升序排列 24 @Override 25 public int compare(Student o1, Student o2) { 26 return o1.id - o2.id; 27 } 28 29 } 30 31 public static class IdDescendingComparator implements Comparator<Student> { 32 // id降序排列 33 @Override 34 public int compare(Student o1, Student o2) { 35 return o2.id - o1.id; 36 } 37 38 } 39 40 public static class AgeAscendingComparator implements Comparator<Student> { 41 // age升序排序 42 @Override 43 public int compare(Student o1, Student o2) { 44 return o1.age - o2.age; 45 } 46 47 } 48 49 public static class AgeDescendingComparator implements Comparator<Student> { 50 // age降序排序 51 @Override 52 public int compare(Student o1, Student o2) { 53 return o2.age - o1.age; 54 } 55 56 } 57 58 public static void printStudents(Student[] students) { 59 for (Student student : students) { 60 System.out.println("Name : " + student.name + ", Id : " + student.id + ", Age : " + student.age); 61 } 62 System.out.println("==========================="); 63 } 64 65 public static void main(String[] args) { 66 Student student1 = new Student("A", 1, 23); 67 Student student2 = new Student("B", 2, 21); 68 Student student3 = new Student("C", 3, 22); 69 70 Student[] students = new Student[] { student3, student2, student1 }; 71 printStudents(students); 72 73 // 下面的sort不加后面的参数会按照内存中的地址排序 74 Arrays.sort(students, new IdAscendingComparator()); 75 printStudents(students); 76 77 Arrays.sort(students, new IdDescendingComparator()); 78 printStudents(students); 79 80 Arrays.sort(students, new AgeAscendingComparator()); 81 printStudents(students); 82 83 Arrays.sort(students, new AgeDescendingComparator()); 84 printStudents(students); 85 86 // 优先队列其实就是个堆 87 Comparator comparator = new AgeAscendingComparator(); 88 Queue<Student> heap = new PriorityQueue<Student>(3, comparator); 89 heap.add(student3); 90 heap.add(student2); 91 heap.add(student1); 92 93 System.out.println("优先队列排序: "); 94 while(!heap.isEmpty()){ 95 Student student = heap.poll(); // 弹出堆顶 96 System.out.println("Name : " + student.name + ", Id : " + student.id + ", Age : " + student.age); 97 } 98 99 } 100 101 }
4、实际问题
小和问题:
1 问题描述: 2 在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。 3 求一个数组的小和。 4 例子: 5 [1,3,4,2,5] 6 1左边比1小的数,没有; 7 3左边比3小的数,1; 8 4左边比4小的数,1、3; 9 2左边比2小的数,1; 10 5左边比5小的数,1、3、4、2; 11 所以小和为1+1+3+1+1+3+4+2=16 12 13 解决思路: 14 使用归并排序中的merge将数组分割,然后合并的时候对比大小(如果小就计算)并使合并的整体有序
1 public class SmallSum { 2 public static int smallSum(int[] arr) { 3 if (arr == null || arr.length < 2) { 4 return 0; 5 } 6 return mergeSort(arr, 0, arr.length - 1); 7 } 8 9 public static int mergeSort(int[] arr, int L, int R) { 10 if (L == R) { 11 return 0; 12 } 13 int mid = L + (R - L) / 2; // 等同于int mid = (L + R) / 2; 14 return mergeSort(arr, L, mid) + mergeSort(arr, mid + 1, R) 15 + merge(arr, L, mid, R); 16 } 17 18 public static int merge(int[] arr, int L, int mid, int R) { 19 int[] help = new int[R - L + 1]; // auxiliary array 20 int i = 0; 21 int p1 = L; 22 int p2 = mid + 1; 23 int res = 0; 24 while (p1 <= mid && p2 <= R) { 25 res += arr[p1] < arr[p2] ? (R - p2 + 1) * arr[p1] : 0; 26 help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; 27 } 28 while (p1 <= mid) { 29 help[i++] = arr[p1++]; 30 } 31 while (p2 <= R) { 32 help[i++] = arr[p2++]; 33 } 34 for (i = 0; i < help.length; i++) { 35 arr[L + i] = help[i]; 36 } 37 return res; 38 } 39 40 public static void main(String[] args) { 41 int[] arr = { 1, 3, 4, 2, 5 }; 42 int[] arr2 = { 1 }; 43 int[] arr3 = { 3, 1 }; 44 System.out.println(smallSum(arr)); 45 System.out.println(smallSum(arr2)); 46 System.out.println(smallSum(arr3)); 47 } 48 }
荷兰国旗问题:
1 荷兰国旗问题 2 给定一个数组 arr,和一个数 num,请把小于 num 的数放在数组的左边,等于 num 的数放在数组的中间,大于 num 的数放在数组的右边。 3 要求额外空间复杂度 O(1),时间复杂度 O(N)
1 public static int[] partition(int[] arr, int L, int R, int num) { 2 int less = L - 1; 3 int more = R + 1; 4 int cur = L; 5 while (cur < more) { 6 if (arr[cur] < num) { 7 swap(arr, cur++, ++less); 8 } else if (arr[cur] > num) { 9 swap(arr, cur, --more); 10 } else { 11 cur++; 12 } 13 } 14 15 // 返回的是等于区域的左边界和右边界 16 if(less+1>more-1){ 17 // 表示不存在等于区域 18 return new int[] { -1, -1 }; 19 } 20 else{ 21 // 等于区域存在 22 return new int[] { less + 1, more - 1 }; 23 } 24 25 }
逆序对问题:
在一个数组中 如果左边的数比右边的数大 则这两个数构成构成一个逆序对 请打印所有逆序对
思路同上面的小和问题
1 public class ReverseSequence { 2 public static void reverseSequence(int[] arr) { 3 if (arr == null || arr.length < 2) { 4 return; 5 } 6 mergeSort(arr, 0, arr.length - 1); 7 } 8 9 public static void mergeSort(int[] arr, int L, int R) { 10 if (L == R) { 11 return; 12 } 13 int mid = L + (R - L) / 2; 14 mergeSort(arr, L, mid); 15 mergeSort(arr, mid + 1, R); 16 merge(arr, L, mid, R); 17 } 18 19 public static void merge(int[] arr, int L, int mid, int R) { 20 int[] help = new int[R - L + 1]; 21 int i = 0; 22 int p1 = L; 23 int p2 = mid + 1; 24 while (p1 <= mid && p2 <= R) { 25 if (arr[p1] > arr[p2]) { 26 System.out.println(arr[p1] + " " + arr[p2]); 27 } 28 help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; 29 } 30 while (p1 <= mid) { 31 help[i++] = arr[p1++]; 32 } 33 while (p2 <= R) { 34 help[i++] = arr[p2++]; 35 } 36 for (i = 0; i < help.length; i++) { 37 arr[L + i] = help[i]; 38 } 39 40 } 41 42 public static void main(String[] args) { 43 int[] arr = { 3, 1, 2, 5, 3 }; 44 ReverseSequence.reverseSequence(arr); 45 } 46 }