Common Sort - 排序 - Java

x33g5p2x  于2022-03-07 转载在 Java  
字(20.7k)|赞(0)|评价(0)|浏览(461)

排序

概念

排序,就是使一串记录,按照其中的某个 或 某些关键字的大小,递增 或 递减 的 排列起来的操作。
平时的上下文中,如果提到排序,通常指的是 排升序(非降序)。
通常意义上的排序,都是指的原地排序(in place sort)
原地排序:就是指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。

稳定性(重要)

两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。

应用 - 举例

1.、各大商城的价格从低到高等

2、中国大学排名

常见的排序算法(8 种)- 总览

直接插入排序

插入排序:非常简单!仅次于冒泡排序。

根据这个思维:第一个数据是有序的,也就是说:在我们遍历的时候,是从下标1 开始的。
具体的操作见下图:

模拟实现 - 插入排序

  1. import java.util.Arrays;
  2. public class DirectInsertionSort {
  3. /*
  4. * 时间复杂度: O(N^2)
  5. * 最好情况: O(N) 数组有序的情况
  6. * 空间复杂度:O(1) 只有 一个 tmp 变量是常驻的
  7. * 稳定性:稳定
  8. * */
  9. public static void insertionSort(int[] array){
  10. for(int i = 1;i < array.length;i++){
  11. int tmp = array[i];
  12. int j = i - 1;
  13. for( ; j >= 0;j-- ){
  14. // 前移
  15. if(tmp < array[j]){
  16. array[j + 1] = array[j];
  17. }else{
  18. break;
  19. }
  20. }
  21. // 插入【无论是找到了合适插入的位置,还是不存在比 tmp更小的值,j自减到 -1.执行的代码都是一样的】
  22. array[j+1] = tmp;
  23. }
  24. }
  25. public static void main(String[] args) {
  26. int[] array = {23,45,56,68,8,9};
  27. insertionSort(array);
  28. System.out.println(Arrays.toString(array));
  29. }
  30. }

稳定性分析

结论

一个稳定的排序,可以实现为 不稳定的排序。
但是,一个本身就不稳定的排序是 无法变成 稳定的排序。
直接插入排序 是 有序的。
它的时间复杂度是 O(N^2);最好情况:O(N【数组有序】
也就是说:对于直接插入排序,数据越有序越快!
由此,不难联想到:直接插入排序 有时候 会用于 优化 排序。
【假设:假设我们有一百万个数据需要排序,在排序的过程中,区间越来越小,数据越来越有序。直接插入排序的时间复杂度为 O(N),N 越来越小,那么,使用 直接插入排序是不是越来越快!也就是说:直接插入排序 有时候会 用于 排序优化】
直接插入排序经常使用在 数据量不多,且整体数据趋于有序的。

  1. import java.util.Random;
  2. public class DirectInsertionSort {
  3. /*
  4. * 时间复杂度: O(N^2)
  5. * 空间复杂度:O(1) 只有 一个 tmp 变量是常驻的
  6. * 稳定性:稳定
  7. * */
  8. public static void insertionSort(int[] array){
  9. for(int i = 1;i < array.length;i++){
  10. int tmp = array[i];
  11. int j = i - 1;
  12. for( ; j >= 0;j-- ){
  13. if(tmp < array[j]){
  14. array[j + 1] = array[j];
  15. }else{
  16. break;
  17. }
  18. }
  19. array[j+1] = tmp;
  20. }
  21. }
  22. // 有序
  23. public static void test1(int capacity){
  24. int[] array = new int[capacity];
  25. for (int i = 0; i < capacity; i++) {
  26. array[i] = i;
  27. }
  28. // 记录开始排序开始时间
  29. long start = System.currentTimeMillis();
  30. insertionSort(array);
  31. // 记录开始排序结束时间
  32. long end = System.currentTimeMillis();
  33. // 输出 整个排序过程的时间
  34. System.out.println(end - start);
  35. }
  36. // 无序
  37. public static void test2(int capacity){
  38. int[] array = new int[capacity];
  39. Random random = new Random();
  40. for (int i = 0; i < capacity; i++) {
  41. array[i] = random.nextInt(capacity);
  42. }
  43. // 记录开始排序开始时间
  44. long start = System.currentTimeMillis();
  45. insertionSort(array);
  46. // 记录开始排序结束时间
  47. long end = System.currentTimeMillis();
  48. // 输出 整个排序过程的时间
  49. System.out.println(end - start);
  50. }
  51. public static void main(String[] args) {
  52. test1(10000);
  53. test2(10000);
  54. }
  55. }

希尔排序

思考

假设,现有 1 00 00 个 数据,如果对着组数据进行排序,使用插入排序。
时间复杂度为 O(N^2)【最坏情况:逆序的情况】
故 1 00 00 * 1 00 00 == 1 亿(量化)
它不是 1 万个数据嘛。那么,我们可以不可以这么去想:将这 一万个数据拆分成 100 组【每组100个数据】,对其中一组进行直接插入排序的时间复杂度为 100*100 ==1 00 00(量化),这样的分组还有99个,也就是将这一百组使用直接插入排序的时间复杂度为 1 00 00 * 1 00 = 1 百万(量化)。
有没有发现,分组过后,时间复杂度效率 提高很多,由1亿 变成了 1百万。
也就是说:如果采用分组的思想,我们会发现 时间复杂度会有一个很大的改变。
而这种分组的思想 就是 希尔排序。

原理

希尔排序又称缩小增量法。希尔排序法的基本思想是:先选定一个整数 n,把待排序文件中所有数据分成 n 组,所有距离为 数据量 / n 的 分在同一组。并且对每一组内的数据进行排序。然后,重复上述 分组 和 排序工作。当分组的组数为 1 是,所有数据 在进行 一个排序。

1、希尔排序 是对直接插入排序的优化。
2、当 group > 1 时都是预排序,目的是让数组更接近于有序。当 group == 1时,数组已经接近有序了,这样就会更快。对于整体而言,可以达到优化的效果。
那么,问题来了!我们怎去确定分多少组,而且越分越少。
【取自清华大学出版的一本书《数据结构》】

科学家的分组思维

现在这组数据,我们相当于只排序了一组数据,就走人了。数组整体还不是有序的。那么,我们该怎么解决这个问题?往下看!

模拟实现 - 希尔排序

  1. import java.util.Arrays;
  2. public class ShellSort {
  3. /*
  4. * 时间复杂度和增量有关系,所以无法得出准确的时间复杂度
  5. * 但只需要记住:在一定的范围里,希尔排序的时间复杂度为 O(N^1.3 ~ N^1.5)
  6. * 空间复杂度为 O(1)
  7. * 稳定性:不稳定
  8. * 判断稳定性的技巧:如果在比较的过程中 发生了 跳跃式交换。那么,就是不稳定的排序。
  9. * */
  10. public static void shell(int[] array,int group){
  11. for (int i = group; i < array.length; i += 1) {
  12. int tmp = array[i];
  13. int j = i-group;
  14. for (; j >= 0; j-=group) {
  15. if(tmp < array[j]){
  16. array[j+group] = array[j];
  17. }else{
  18. break;
  19. }
  20. }
  21. array[j+group] = tmp;
  22. }
  23. }
  24. public static void shellSort(int[] array){
  25. int group = array.length;
  26. // 预排序
  27. while(group > 1){
  28. // 第一次分组委 数组的长度,即 头尾判断。
  29. // 其后,每次分组个数,缩小一倍。
  30. shell(array,group);
  31. group /= 2;
  32. }
  33. // 最后调整
  34. shell(array,1);
  35. }
  36. public static void main(String[] args) {
  37. int[] array ={12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
  38. shellSort(array);
  39. System.out.println(Arrays.toString(array));
  40. }
  41. }

总结

其实 希尔排序就是一个直接插入排序。

选择排序

直接选择排序 - 原理

优化

定义 一个 变量, 用来记录 此时的 i 后面最小值的下标。等 j 遍历完了,最小值的下标也就拿到了。此时,再进行交换。
这样就不必让上面那样,遇到比 i下标元素 小的,就交换。

代码如下

  1. import java.util.Arrays;
  2. public class SelectSort {
  3. /*
  4. * 稳定性: 不稳定 见附图
  5. * 时间复杂度:O(N^2) 》》 外层循环 n -1,内层循环 n -1
  6. * 空间复杂度:O(1)
  7. * */
  8. public static void selectSort(int[] array){
  9. for (int i = 0; i < array.length-1; i++) {
  10. int index = i;
  11. for (int j = i + 1; j < array.length; j++) {
  12. if(array[index] > array[j]){
  13. index = j;
  14. }
  15. }
  16. int tmp = array[i];
  17. array[i] = array[index];
  18. array[index] = tmp;
  19. }
  20. }
  21. public static void main(String[] args) {
  22. int[] array = {12,6,10,3,5};
  23. selectSort(array);
  24. System.out.println(Arrays.toString(array));
  25. }
  26. }

附图

双向选择排序 (了解)

每一次从无序区间选出最小 + 最大的元素,存放在无序区间的最前和最后,直到全部待排序的数据元素排完 。

代码如下

  1. import java.util.Arrays;
  2. public class SelectSortOP {
  3. public static void selectSortOP(int[] array){
  4. int low = 0;
  5. int high = array.length - 1;
  6. // [low,high] 表示整个无序区间
  7. while(low < high){
  8. int min = low;
  9. int max = low;
  10. for (int i = low+1; i <= high; i++) {
  11. if(array[i] < array[min]){
  12. min = i;
  13. }
  14. if(array[i] > array[max]){
  15. max = i;
  16. }
  17. }
  18. swap(array,min,low);
  19. if(max == low){
  20. max = min;
  21. }
  22. swap(array,max,high);
  23. low++;
  24. high--;
  25. }
  26. }
  27. public static void swap(int[] array,int x,int y){
  28. int tmp = array[x];
  29. array[x] = array[y];
  30. array[y] = tmp;
  31. }
  32. public static void main(String[] args) {
  33. int[] array = {9, 5, 2, 7, 3, 6, 8 };
  34. selectSortOP(array);
  35. System.out.println(Arrays.toString(array));
  36. }
  37. }

堆排序

基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数。
注意: 排升序要建大堆;排降序要建小堆.
这个我就不讲,因为我在堆/优先级中讲的很清楚!
有兴趣的,可以点击 链接关键字 ,跳转到该文章,该内容在 文章目录最后面。
这里我们就直接上代码。

代码

  1. import java.util.Arrays;
  2. public class HeapSort {
  3. public static void main(String[] args) {
  4. int[] array = {12,8,5,4,10,15};
  5. creationHeap(array);// 建堆的时间复杂度:O(N)
  6. System.out.println(Arrays.toString(array));
  7. heapSort(array);// 堆排序的时间复杂度:O(N * log2 N)
  8. // 空间复杂度:O(1)
  9. System.out.println(Arrays.toString(array));
  10. }
  11. // 创建一个大根堆
  12. public static void creationHeap(int[] array){
  13. for (int parent = (array.length-1-1)/2; parent >= 0; parent--) {
  14. shiftDown(array,parent,array.length);
  15. }
  16. }
  17. public static void heapSort(int[] array){
  18. /*
  19. * 时间复杂度:O(N * log2 N)
  20. * 空间复杂度:O(1)
  21. * 稳定性:不稳定
  22. * */
  23. int end = array.length - 1;
  24. while(end>0){
  25. int tmp = array[end];
  26. array[end] = array[0];
  27. array[0] = tmp;
  28. shiftDown(array,0,end);
  29. end--;
  30. }
  31. }
  32. // 向下调整
  33. public static void shiftDown(int[] array,int parent,int len){
  34. int child = parent * 2 + 1;// 做孩纸
  35. while(child < len){
  36. // 获取左右子树最大值的下标
  37. if(child+1 < len && (array[child] < array[child+1])){
  38. child++;
  39. }
  40. if(array[child] > array[parent]){
  41. int tmp = array[child];
  42. array[child] = array[parent];
  43. array[parent] = tmp;
  44. parent = child;
  45. child = parent * 2 + 1;
  46. }else{
  47. break;
  48. }
  49. }
  50. }
  51. }

冒泡排序

代码如下 - 未优化

  1. import java.util.Arrays;
  2. /*
  3. * 时间复杂度:O(N^2) 【无论是最好情况,还是最坏情况,时间复杂度都不变】
  4. * 空间复杂度:O(1)
  5. * 稳定性:稳定【未发生跳跃式交换】
  6. * */
  7. public class BubbleSort {
  8. public static void bubbleSort(int[] array){
  9. // 比较的趟数 = 数组的长度 - 1 【 0 ~ 3 一共 4趟】
  10. for (int i = 0; i < array.length-1; i++) {
  11. // 比较完一趟后,可以比较的元素个数减一。【因为靠后的数据已经有序】
  12. // 内循环中,之所以要减一个 1,是因为防止 下面的if语句 发生 数组越界异常
  13. for(int j = 0;j< array.length-1-i;j++){
  14. if(array[j] > array[j+1]){
  15. int tmp = array[j];
  16. array[j] = array[j+1];
  17. array[j+1] = tmp;
  18. }
  19. }
  20. }
  21. }
  22. public static void main(String[] args) {
  23. int[] array = {12,6,10,3,5};
  24. bubbleSort(array);
  25. System.out.println(Arrays.toString(array));
  26. }
  27. }

代码优化思维

代码如下 - 优化

  1. import java.util.Arrays;
  2. public class BubbleSort {
  3. /*
  4. * 时间复杂度:O(N^2)
  5. * 最好情况【数组有序】可以达到 O(N)
  6. * 空间复杂度:O(1)
  7. * 稳定性:稳定【未发生跳跃式交换】
  8. * */
  9. public static void bubbleSort(int[] array){
  10. for (int i = 0; i < array.length-1; i++) {
  11. boolean flag = true;
  12. for(int j = 0;j< array.length-1-i;j++){
  13. if(array[j] > array[j+1]){
  14. int tmp = array[j];
  15. array[j] = array[j+1];
  16. array[j+1] = tmp;
  17. flag = false;// 表示这一趟比较,数组是无序的
  18. }
  19. }
  20. // flag == true
  21. if(flag){
  22. break;
  23. }
  24. }
  25. }
  26. public static void main(String[] args) {
  27. // 前半段无序,后半段有序
  28. int[] array = {2,3,1,4,5};
  29. bubbleSort(array);
  30. System.out.println(Arrays.toString(array));
  31. }
  32. }

未优化 和 优化代码 运行速度比较

  1. public class BubbleSort {
  2. // 优化
  3. public static void bubbleSort2(int[] array){
  4. for (int i = 0; i < array.length-1; i++) {
  5. boolean flag = true;
  6. for(int j = 0;j< array.length-1-i;j++){
  7. if(array[j] > array[j+1]){
  8. int tmp = array[j];
  9. array[j] = array[j+1];
  10. array[j+1] = tmp;
  11. flag = false;
  12. }
  13. }
  14. // flag == true
  15. if(flag){
  16. break;
  17. }
  18. }
  19. }
  20. // 未优化
  21. public static void bubbleSort1(int[] array){
  22. for (int i = 0; i < array.length-1; i++) {
  23. for(int j = 0;j< array.length-1-i;j++){
  24. if(array[j] > array[j+1]){
  25. int tmp = array[j];
  26. array[j] = array[j+1];
  27. array[j+1] = tmp;
  28. }
  29. }
  30. }
  31. }
  32. public static void main(String[] args) {
  33. int[] array = new int[10000];
  34. for (int i = 0; i < array.length; i++) {
  35. array[i] = i;
  36. }
  37. long start = System.currentTimeMillis();
  38. bubbleSort2(array);// 优化
  39. long end = System.currentTimeMillis();
  40. System.out.println(end - start);// 输出排序所需时间
  41. start = System.currentTimeMillis();
  42. bubbleSort1(array);// 未优化
  43. end = System.currentTimeMillis();
  44. System.out.println(end - start);//输出排序所需时间
  45. }
  46. }

快速排序 - 重点

原理

1、从待排序区间选择一个数,作为基准值(pivot)
2、Partition(分割):遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边。
3、采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1.代表已经有序,或者小区间的长度 == 0,代表没有数据。

总结

快速排序,其实说白了 和二叉树]很像,先根,再左,后右。利用递归去实现!

程序框架

  1. public class QuickSort {
  2. public static void quickSort(int[] array){
  3. quick(array,0, array.length);
  4. }
  5. public static void quick(int[] array,int start,int end){
  6. if(start >= end){
  7. return;
  8. }
  9. int pivot = partiton(array,start,end);
  10. quick(array,start,pivot-1);// 递归左边
  11. quick(array,pivot+1,end);// 递归右边
  12. }
  13. // 分割 - 找基准
  14. private static int partiton(int[] array,int start,int end){
  15. }
  16. }

完善 partition 部分

  1. // 分割 - 找基准
  2. private static int partiton(int[] array,int start,int end){
  3. int tmp = array[start];
  4. while(start < end){
  5. while(start < end && array[end] >= tmp){
  6. end--;
  7. }
  8. // 此时 end 下标 元素的值 是 小于 tmp的。
  9. array[start] = array[end];
  10. while(start<end && array[start] <= tmp){
  11. start++;
  12. }
  13. //此时 start 下标元素的值 是 大于 tmp的。
  14. array[end] = array[start];
  15. }
  16. // start 和 end 相遇了,将 tmp 赋予 它们相遇下标指向的空间
  17. array[start] = tmp;
  18. return start;
  19. }

代码细节部分

总程序 - 未优化

  1. import java.util.Arrays;
  2. public class QuickSort {
  3. /*
  4. * 时间复杂度:O(N^2) 【数据有序或者逆序的情况】
  5. * 最好情况【每次可以均匀的分割待排序序列】:O(N * log2 N)
  6. * 空间复杂度:O(N)[单分支的一棵树]
  7. * 最好:log2 N
  8. * 稳定性:不稳定
  9. * */
  10. public static void quickSort(int[] array){
  11. quick(array,0, array.length-1);
  12. }
  13. public static void quick(int[] array,int start,int end){
  14. if(start >= end){
  15. return;
  16. }
  17. int pivot = partiton(array,start,end);
  18. quick(array,start,pivot-1);// 递归左边
  19. quick(array,pivot+1,end);// 递归右边
  20. }
  21. // 分割 - 找基准
  22. private static int partiton(int[] array,int start,int end){
  23. int tmp = array[start];
  24. while(start < end){
  25. while(start < end && array[end] >= tmp){
  26. end--;
  27. }
  28. // 此时 end 下标 元素的值 是 小于 tmp的。
  29. array[start] = array[end];
  30. while(start<end && array[start] <= tmp){
  31. start++;
  32. }
  33. array[end] = array[start];
  34. }
  35. array[start] = tmp;
  36. return start;
  37. }
  38. public static void main(String[] args) {
  39. int[] array = {6,1,2,7,9,3,4,5,10,8};
  40. quickSort(array);
  41. System.out.println(Arrays.toString(array));
  42. }
  43. }

快速排序 的 时间 与 空间复杂度分析

堆排序 与 快排 的区别

细心的朋友会发现 堆排序 和 快排 的 时间复杂度在最好情况下 都是N* log2 N。
那么,两者又有什么区别?
堆排序,无论最好还是最坏情况,时间复杂度都是N* log2 N。空间复杂度 O(1)
那么,又为什么快排 比 堆排序 要快?
其实再细一点说 :在两个排序的时间复杂度都为 N* log2 N时,其实连着前面还有 一个 k【K * N* log2 N 】,只不过快排前面的K要小一点。所以快排要快一点。
在对空间复杂度没有要求的情况: 快排
对空间复杂度有要求的情况,或者说对数据的序列也要要求: 堆排

细节拓展

if语句中 比较大小的代码中 等号是不能省略的

当 下面框选的代码 没有等号时,会造成死循环。

我就改了一下,末尾元素的值。

那么,问题来了:为什么没有等号就死循环了?

所以,在 写快排的时候,比较大小的代码,记住一定要加上等号!!!!!

目前版本的 快排代码 不支持 大量数据进行排序 - 会导致栈溢出。

这是因为 我们递归的太深了,1百万数据,4百万字节。
1TB等于1024GB;1GB等于1024MB;1MB等于1024KB;1KB等于1024Byte(字节);1Byte等于8bit(位);

有的朋友会说:这才多大啊?栈怎么会被挤爆?
这是因为在递归的时候,开辟的栈帧【函数的信息,参数等等等…都有】,所以,每次开辟的栈帧不止 4byte。故栈被挤爆了。

所以,我们要优化快排的 代码。【优化:数据有序的情况】

基准值的选择 - 优化前的知识补充

1、选择边上(左或者右) 【重点,上面使用的就是这种方法】
2、随机选择(针对 有序数据)【了解】

3、几数取中(常见的就是三数取中):array[left],array[mid] ,array[right]中 大小为 中间值的为基准值【优化的关键】

快速排序(几数取中法 优化)

  1. import java.util.Arrays;
  2. public class QuickSort {
  3. /*
  4. * 时间复杂度:O(N^2) 【数据有序或者逆序的情况】
  5. * 最好情况【每次可以均匀的分割待排序序列】:O(N * log2 N)
  6. * 空间复杂度:O(N)[单分支情况]
  7. * 最好:log2 N
  8. * 稳定性:不稳定
  9. * */
  10. public static void quickSort(int[] array){
  11. quick(array,0, array.length-1);
  12. }
  13. public static void quick(int[] array,int start,int end){
  14. if(start >= end){
  15. return;
  16. }
  17. // 在找基准之前,先确定 start 和 end 的 中间值。[三数取中法]
  18. int midValIndex = findMidValIndex(array,start,end);
  19. //将它 与 start 交换。这样后面的程序,就不用改动了。
  20. swap(array,start,midValIndex);
  21. int pivot = partiton(array,start,end);
  22. quick(array,start,pivot-1);// 递归左边
  23. quick(array,pivot+1,end);// 递归右边
  24. }
  25. // 确定基准值下标
  26. private static int findMidValIndex(int[] array,int start,int end){
  27. // 确定 start 和 end 的中间下标
  28. int mid = start + ((end - start)>>>1);// == (start + end)/ 2
  29. // 确定 mid、start、end 三个下标,谁指向的元素是三个元素中的中间值
  30. if(array[end] > array[start]){
  31. if(array[start] > array[mid]){
  32. return start;
  33. }else if(array[mid] > array[end]){
  34. return end;
  35. }else{
  36. return mid;
  37. }
  38. }else{
  39. // array[start] >= array[end]
  40. if(array[end] > array[mid]){
  41. return end;
  42. }else if(array[mid] > array[start]){
  43. return start;
  44. }else {
  45. return mid;
  46. }
  47. }
  48. }
  49. // 交换两个下标元素
  50. private static void swap(int[] array,int x,int y){
  51. int tmp = array[x];
  52. array[x] = array[y];
  53. array[y] = tmp;
  54. }
  55. // 分割 - 找基准
  56. private static int partiton(int[] array,int start,int end){
  57. int tmp = array[start];
  58. while(start < end){
  59. while(start < end && array[end] >= tmp){
  60. end--;
  61. }
  62. // 此时 end 下标 元素的值 是 小于 tmp的。
  63. array[start] = array[end];
  64. while(start<end && array[start] <= tmp){
  65. start++;
  66. }
  67. array[end] = array[start];
  68. }
  69. array[start] = tmp;
  70. return start;
  71. }
  72. // 有序
  73. public static void test1(int capacity){
  74. int[] array = new int[capacity];
  75. for (int i = 0; i < capacity; i++) {
  76. array[i] = i;
  77. }
  78. long start = System.currentTimeMillis();
  79. quickSort(array);
  80. long end = System.currentTimeMillis();
  81. System.out.println(end - start);
  82. }
  83. public static void main(String[] args) {
  84. test1(100_0000);
  85. int[] array = {6,1,2,7,9,3,4,5,10,6};
  86. quickSort(array);
  87. System.out.println(Arrays.toString(array));
  88. }
  89. }

优化总结

1、选择基准值很重要,通常使用几数取中法
2、partition 过程中把和基准值相等的数也选择出来

3、待排序区间小于一个阈(yù)值【临界值】
随着不断的划分基准,数组逐渐趋于有序,而区间随着递归也在减小。所以,利用 直接插入排序的特性【越有序越快】,来进一步优化 快排。

拓展 快速排序 - 非递归实现

非递归实现快速排序的思维

代码如下

  1. import java.util.Arrays;
  2. import java.util.Stack;
  3. public class QuickSortNonRecursion {
  4. public static void quickSort(int[] array){
  5. Stack<Integer> stack = new Stack<>();
  6. int left = 0;
  7. int right = array.length-1;
  8. int pivot = partiton(array,left,right);
  9. if(pivot > left+1){
  10. stack.push(left);
  11. stack.push(pivot-1);
  12. }
  13. if(pivot < right -1){
  14. stack.push(pivot+1);
  15. stack.push(right);
  16. }
  17. while(!stack.isEmpty()){
  18. right = stack.pop();
  19. left = stack.pop();
  20. pivot = partiton(array,left,right);
  21. if(pivot>left+1){
  22. stack.push(left);
  23. stack.push(pivot-1);
  24. }
  25. if (pivot<right-1){
  26. stack.push(pivot+1);
  27. stack.push(right);
  28. }
  29. }
  30. }
  31. public static int partiton(int[] array,int start,int end){
  32. int tmp = array[start];
  33. while(start<end){
  34. while(start<end && array[end] >=tmp){
  35. end--;
  36. }
  37. array[start] = array[end];
  38. while (start<end && array[start] <= tmp){
  39. start++;
  40. }
  41. array[end] = array[start];
  42. }
  43. array[start] = tmp;
  44. return start;
  45. }
  46. public static void main(String[] args) {
  47. int[] array = {12,5,8,1,10,15};
  48. quickSort(array);
  49. System.out.println(Arrays.toString(array));
  50. }
  51. }

归并排序 - 重点

知识铺垫 : 二路合并

将两个有序表合并成一个有序表,称为二路归并。【简单说就是 将两个有序数组合并为一个有序数组,称为二路合并】

二路合并的代码如下

  1. import java.util.Arrays;
  2. public class MergeSort {
  3. /*
  4. * array1 已有序
  5. * array2 已有序
  6. * */
  7. public static int[] mergeArrays(int[] array1,int[] array2){
  8. if(array1 == null || array2 == null){
  9. return array1 == null ? array2: array1;
  10. }
  11. int[] arr = new int[array1.length + array2.length];
  12. int i = 0;// arr 的 遍历变量
  13. int s1 = 0;//array1 的 遍历变量
  14. int s2 = 0;//array2 的 遍历变量
  15. while(s1 < array1.length && s2 < array2.length){
  16. if(array1[s1] > array2[s2]){
  17. arr[i++] = array2[s2++];
  18. // s2++;
  19. // i++;
  20. }else{
  21. arr[i++] = array1[s1++];
  22. // s1++;
  23. // i++;
  24. }
  25. }
  26. // 循环结束,有一个数组的元素已经全部存入
  27. // 接下来就是将另一个数组的元素放入 arr 中
  28. while (s1 < array1.length){
  29. arr[i++] = array1[s1++];
  30. // i++;
  31. // s1++;
  32. }
  33. while (s2 < array2.length){
  34. arr[i++] = array2[s2++];
  35. // i++;
  36. // s2++;
  37. }
  38. return arr;
  39. }
  40. public static void main(String[] args) {
  41. int[] array1 = {1,6,7,10};
  42. int[] array2 = {2,3,4,9};
  43. int[] mergeArray = mergeArrays(array1,array2);
  44. System.out.println(Arrays.toString(mergeArray));
  45. }
  46. }

归并排序 - 原理

归并排序(MERGE - SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

难点1 - 如何将一个数组拆分成一个个单独数组【每个数组里只包含一个元素】。

难点2 - 合并

归并排序的程序框架

  1. public class MergeSort {
  2. // 归并排序的调用“接口”
  3. public static int[] mergeSort(int[] array){
  4. if(array == null){
  5. return array;
  6. }
  7. mergeSortFunc(array,0,array.length-1);
  8. return array;
  9. }
  10. // 归并排序实现
  11. private static void mergeSortFunc(int[] array,int low,int high){
  12. if(low >= high){
  13. return;
  14. }
  15. // 递归分解
  16. // int mid = (high + low) >>> 1
  17. int mid = low + ((high - low) >>> 1);
  18. mergeSortFunc(array,low,mid);// 左边
  19. mergeSortFunc(array,mid+1,high);// 右边
  20. // 合并
  21. merge(array,low,mid,high);
  22. }
  23. private static void merge(int[] array,int low,int mid,int high){
  24. }
  25. }

合并程序的完善

其实这个并不难,跟我前面做的知识铺垫的思路是一样的。
需要注意的是:
1、我们的参数中 只有一个数组
2、数组 arr ,只是一个临时数组,用来存储 合并之后的结果。
3、在将 arr 数组 存储的结果,转移到 原本数组的时候,注意赋值的位置!

  1. private static void merge(int[] array,int low,int mid,int high){
  2. // 获取 区间之内的元素个数,加一 是因为 零下标元素也算一个元素。
  3. int[] arr = new int[high - low +1];
  4. // 左边 区间 【你可以理解为 有序数组 array1的起始与结束下标位置】
  5. int start1 = low;
  6. int end1 = mid;
  7. // 右边 区间【你可以理解为 有序数组 array2的起始与结束下标位置】
  8. int start2 = mid+1;
  9. int end2 = high;
  10. int i = 0;
  11. while (start1 <= end1 && start2 <= end2){
  12. if(array[start1] > array[start2]){
  13. arr[i++] = array[start2++];
  14. }else{
  15. arr[i++] = array[start1++];
  16. }
  17. }
  18. while(start1 <= end1){
  19. arr[i++] = array[start1++];
  20. }
  21. while(start2 <= end2){
  22. arr[i++] = array[start2++];
  23. }
  24. // 将 arr 存储的 合并数据,转换到原本数组上。
  25. // 注意 array 数组中括号的下标的位置。
  26. for (int j = 0; j < arr.length; j++) {
  27. array[low++] = arr[j];
  28. }
  29. }
附图

归并排序 - 总程序

  1. import java.util.Arrays;
  2. public class MergeSort {
  3. /*
  4. * 时间复杂度:N * log2 N
  5. * 空间复杂丢:O(N)
  6. * 稳定性:稳定
  7. * */
  8. public static int[] mergeSort(int[] array){
  9. if(array == null){
  10. return array;
  11. }
  12. mergeSortFunc(array,0,array.length-1);
  13. return array;
  14. }
  15. private static void mergeSortFunc(int[] array,int low,int high){
  16. if(low >= high){
  17. return;
  18. }
  19. // int mid = (high + low) >>> 1
  20. int mid = low + ((high - low) >>> 1);
  21. mergeSortFunc(array,low,mid);// 左边
  22. mergeSortFunc(array,mid+1,high);// 右边
  23. merge(array,low,mid,high);
  24. }
  25. private static void merge(int[] array,int low,int mid,int high){
  26. int[] arr = new int[high - low +1];
  27. int start1 = low;
  28. int end1 = mid;
  29. int start2 = mid+1;
  30. int end2 = high;
  31. int i = 0;
  32. while (start1 <= end1 && start2 <= end2){
  33. if(array[start1] > array[start2]){
  34. arr[i++] = array[start2++];
  35. }else{
  36. arr[i++] = array[start1++];
  37. }
  38. }
  39. while(start1 <= end1){
  40. arr[i++] = array[start1++];
  41. }
  42. while(start2 <= end2){
  43. arr[i++] = array[start2++];
  44. }
  45. for (int j = 0; j < arr.length; j++) {
  46. array[low++] = arr[j];
  47. }
  48. }
  49. public static void main(String[] args) {
  50. int[] array = {1,6,7,10,2,3,4,9};
  51. mergeSort(array);
  52. System.out.println(Arrays.toString(array));
  53. }
  54. }

归并排序 - 时间与空间复杂度分析、稳定性

归并排序 - 非递归实现

代码如下

  1. import java.util.Arrays;
  2. public class MergeSortNonRecursion {
  3. public static void mergeSort(int[] array){
  4. //归并排序非递归实现
  5. int groupNum = 1;// 每组的数据个数
  6. while(groupNum < array.length){
  7. // 无论数组含有几个元素, 数组每次都需要从下标 0位置,开始遍历。
  8. for(int i = 0;i<array.length;i+= groupNum * 2){
  9. int low = i;
  10. int mid = low + groupNum -1;
  11. // 防止越界【每组的元素个数,超过了数组的长度】
  12. if(mid >= array.length){
  13. mid = array.length-1;
  14. }
  15. int high = mid + groupNum;
  16. // 防止越界【超过了数组的长度】
  17. if(high >= array.length){
  18. high = array.length-1;
  19. }
  20. merge(array,low,mid,high);
  21. }
  22. groupNum *= 2;//每组的元素个数扩大到原先的两倍。
  23. }
  24. }
  25. public static void merge(int[] array,int low,int mid,int high){
  26. // high 与 mid 相遇,说明 此时数组分组只有一组,也就说没有另一组的数组与其合并
  27. // 即数组已经有序了,程序不用再往下走。
  28. if(high == mid){
  29. return;
  30. }
  31. int[] arr = new int[high -low + 1];
  32. int start1 = low;
  33. int end1 = mid;
  34. int start2 = mid+1;
  35. int end2 = high;
  36. int i = 0;
  37. while(start1 <= end1 && start2 <= end2){
  38. if(array[start1]>array[start2]){
  39. arr[i++] = array[start2++];
  40. }else{
  41. arr[i++] = array[start1++];
  42. }
  43. }
  44. while (start1 <= end1){
  45. arr[i++] = array[start1++];
  46. }
  47. while(start2 <= end2){
  48. arr[i++] = array[start2++];
  49. }
  50. for (int j = 0; j < arr.length; j++) {
  51. array[low++] = arr[j];
  52. }
  53. }
  54. public static void main(String[] args) {
  55. int[] array = {12,5,8,7,3,4,1,10};
  56. mergeSort(array);
  57. System.out.println(Arrays.toString(array));
  58. }
  59. }

海量数据的排序问题

外部排序:排序过程需要在磁盘等外部存储进行的排序
【内部排序:排序过程需要在 内存上进行排序】
前提:内存只有 1G,需要排序的数据有 100G
因为内存中无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序。
1、先把文件切分成 200 份,每个512M

2、分别对 512M 的数据量 进行排序,因为 内存已经被分割了,512M < 1G 内存放得下。所以任何排序方式都可以,
3、进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了

小总结

目前,我们讲了八种排序:直接插入排序、希尔排序、直接选择排序,双向选择排序、冒泡排序,堆排序、快速排序,归并排序。
其中稳定的排序:插入排序,冒泡排序,归并排序,一共三种。
?
另外,堆排序、归并排序、快速排序的时间复杂度都是 N * log2 N。
如果,你想速度快,就用快排。
如果,你想稳定,就用归并。
如果,你想空间复杂度低,就用堆排。

排序总结

排序方法最好(时间复杂度)平均(时间复杂度)最坏(时间复杂度)空间复杂度稳定性
冒泡排序O(N)O(N^2)O(N^2)O(1)稳定
插入排序O(N)O(N^2)O(N^2)O(1)稳定
选择排序O(N^2)O(N^2)O(N^2)O(1)不稳定
希尔排序O(N)O(N^1.3)O(N^2)O(1)不稳定
堆排序O(N * log2 N)O(N * log2 N)O(N * log2 N)O(1)不稳定
快速排序O(N * log2 N)O(N * log2 N)O(N ^ 2)O(N)不稳定
归并排序O(N * log2 N)O(N * log2 N)O(N * log2 N)O(N)稳定

不常见的排序 - 不基于比较的排序(了解)

基数排序

它的思路:假设待排序的数据类型是 整形/十进制数,每个数据 分别按照 个,百,千,万的大小,放入拿出对应编号空间,其最终的结果就是有序的。
放入拿出的次数 取决于 这组数据中 最大值的位数。

代码如下

  1. import java.util.Arrays;
  2. public class RadixSort {
  3. // 基数排序功能 实际功能实现方法
  4. private static void radixSortFunc(int[] array,int maxDigit){
  5. int mode = 10 ; // 十进制
  6. int divide = 1;// 将 数值上的每个数“分割”,方便获取数据的一个位上的值
  7. // 每个数据从个位到最大值的最高位,按照其位上的大小 放出 拿出
  8. for (int i = 0; i < maxDigit; i++,mode *= 10,divide *=10) {
  9. // 考虑 负数的 情况,0~9 对应负数,10 ~ 19 对应正数
  10. // 行 对应的是编号, 列 对应的存储的数据
  11. int[][] counter = new int[mode*2][0];
  12. for (int j = 0; j < array.length; j++) {
  13. int number = ((array[j] % mode)/divide) + mode; /*获取 数据对应 空间的编号 */
  14. counter[number] = arrayAppend(counter[number],array[j]);
  15. }
  16. int pos = 0;
  17. for (int[] number:counter) {
  18. for (int val:number) {
  19. array[pos++] = val;
  20. }
  21. }
  22. }
  23. }
  24. // 添加 元素
  25. private static int[] arrayAppend(int[] arr,int value){
  26. arr = Arrays.copyOf(arr,arr.length+1);
  27. arr[arr.length-1] = value;
  28. return arr;
  29. }
  30. // 基数排序 功能调用方法“窗口”
  31. public static void radixSort(int[] array){
  32. int maxNumLength = getNumLength(array);
  33. radixSortFunc(array,maxNumLength);
  34. }
  35. // 获取最大值的位数 - 功能“窗口”
  36. private static int getNumLength(int[] array){
  37. int maxVal = getMaxValue(array);
  38. return getMaxDigit(maxVal);
  39. }
  40. //获取最大值
  41. private static int getMaxValue(int[] array){
  42. int maxValue= array[0];
  43. for (int value: array) {
  44. if(value > maxValue){
  45. maxValue = value;
  46. }
  47. }
  48. return maxValue;
  49. }
  50. // 获取最大值的位数 - 执行
  51. private static int getMaxDigit(int num){
  52. if(num == 0){
  53. return 0;
  54. }
  55. int len = 0;
  56. while(num > 0){
  57. len++;
  58. num /= 10;
  59. }
  60. return len;
  61. }
  62. // 程序入口
  63. public static void main(String[] args) {
  64. int[] array = {124,366,170,52,200,78,468};
  65. radixSort(array);
  66. System.out.println(Arrays.toString(array));
  67. }
  68. }

捅排序

计数排序

在使用 计数排序时,需注意以下几点:
1、确定基数排序的大小
2、这个计数排序 适用的范围【假设数据中最小值 10000,最大 12000,那么,我们的计数数组容量只需要2001(0 下标也算入) 就够了。不需要创建 12000容量,避免空间浪费】
计数数组 计数也简单,用元素值减去 10000(最小值) 就行了
3、必须找到数据中的最大值 和 最小值,锁定计数数组的长度:max - min + 1
4、 拿出数据的时候,记得将减去的 最小值 加上,再进行对原始数组的覆写。

代码如下

  1. import java.util.Arrays;
  2. /*
  3. * 时间复杂度:O(N)
  4. * 空间复杂度: O(M) : M 表示 当前数据的范围
  5. * 【空间 换 时间】
  6. * 稳定性: 当前代码是不稳定,本质是稳定的。
  7. * 在借助一个 数组来存储 每个元素排序后,最后出现的位置,
  8. * 拿出来的时候,就能确定位置。致使该排序 稳定。 - 见附图
  9. * */
  10. public class CountingSort {
  11. public static void countingSort(int[] array){
  12. int maxVal = array[0];
  13. int minVal = array[0];
  14. for (int i = 0; i < array.length; i++) {
  15. if (array[i] > maxVal){
  16. maxVal = array[i];
  17. }
  18. if(array[i] < minVal){
  19. minVal =array[i];
  20. }
  21. }
  22. // 当循环结束,获得了 数据的最大值 和 最小值
  23. // 可以确定计数数组的容量
  24. int[] count = new int[maxVal -minVal +1];
  25. for (int i = 0; i < array.length; i++) {
  26. // 提高空间利用率
  27. count[ array[i] - minVal ]++;
  28. }
  29. // 此时,计数数组 已经把array数组当中,每个元素的出现次数统计好了
  30. // 接下来,只需要遍历计数数组,把 数据 覆写 到 array当中。
  31. int indexArray = 0; // 用于遍历 array数组,标记 覆写的位置。
  32. for (int i = 0; i < count.length; i++) {
  33. while(count[i]>0){
  34. // 这里一定要加上减去minVal,因为 下标 i 不一定 在 array 数组中出现过。
  35. array[indexArray++] = i + minVal;// 拿出来的时候,记得将减去的值加上
  36. // indexArray++;
  37. count[i]--;
  38. }
  39. }
  40. }
  41. public static void main(String[] args) {
  42. int[] array = {5,0,2,3,4,5,2};
  43. countingSort(array);
  44. System.out.println(Arrays.toString(array));
  45. }
  46. }

附图 - 是目前的计数排序稳定

相关文章

最新文章

更多

目录