将几个常用的算法,使用Java代码书写了一遍。其中每一种算法的思想、复杂度、稳定性在网上随处可见,因此本文只做一个简单的叙述;主要说一说,在手撸代码的时候,想起每一个算法的关键点,那么就可以快速的进行代码的书写。
先将排序中用到的一些代码先行写出,方便后续排序中代码的理解。
测试程序
用于测试排序结果的正确性,数组的大小可以根据自己的需求选择;数据的范围自己确定可以生成相应的随机数;可以记录系统的时间,测试排序过程所用到的时间。

1 import java.util.Random; 2 3 /** 4 * @author: wooch 5 * @create: 2020/02/11 6 */ 7 public class TestSort { 8 public static void main(String[] args) { 9 int len = 13; 10 int[] arr = new int[len]; 11 Random random = new Random(); 12 for (int i = 0; i < len; i++) { 13 arr[i] = random.nextInt(33) + 1; 14 } 15 16 TenSort ts = new TenSort(); 17 18 System.out.println("排序前:"); 19 for (int num : arr) { 20 System.out.print(num + " "); 21 } 22 System.out.println(" 开始排序"); 23 24 long startTime = System.currentTimeMillis(); 25 26 //ts.bubbleSort(arr);//7ms 27 //ts.selectSort(arr);//6ms 28 //ts.insertSort(arr);//8ms 29 //ts.shellSort(arr);//1ms 30 //ts.quickSort(arr); 31 //ts.mergeSort(arr); 32 ts.heapSort(arr); 33 34 35 long endTime = System.currentTimeMillis(); 36 System.out.println(" 排序所用时间:" + (endTime - startTime) + "ms"); 37 38 System.out.println("排序后:"); 39 for (int num : arr) { 40 System.out.print(num + " "); 41 } 42 } 43 }
排序算法中的两个函数
一个swap函数,用于交换数组中两个数的位置;printArr函数,可以在在排序过程中使用,查看某一趟的排序结果。
1 private void swap(int[] arr, int i, int j) { 2 int num = arr[i]; 3 arr[i] = arr[j]; 4 arr[j] = num; 5 }
1 private void printArr(int[] arr) { 2 for (int num : arr) { 3 System.out.print(num + " "); 4 } 5 System.out.println(); 6 }
冒泡排序
每一趟排序就是将最大的数据放入到数组的最后。因此关键点有两个:
- 会有多少趟排序,答案为【数组长度-1】趟就够了。换个角度想,最后一个数肯定是数组中最小的,肯定放在数组的第一位,因此不用在排一次序,此时最后一趟可以取消掉,因此只需要【数组长度-1】趟排序。
- 每一趟排序的过程中将最大的数选出来,并且放到数组在该趟的最后位置。例如第一趟肯定选择是最大的数据,那么放在数组的最后一个位置,那么下标为【数组长度-1】,当趟数i是从1开始计数的时候,最后的【j+1】自然就是该趟的最后一个位置。
1 public void bubbleSort(int[] arr) { 2 int len = arr.length; 3 for (int i = 1; i < len; i++) { 4 boolean flag = true; 5 for (int j = 0; j < len - i; j++) { 6 if (arr[j] > arr[j + 1]) { 7 swap(arr, j, j + 1); 8 flag = false; 9 } 10 } 11 if (flag) { 12 break; 13 } 14 } 15 }
因此冒泡排序的关键点就是每次将最大的数放在未排序数组部分的最后一个位置就可以了。
关于优化:当整个数组已经有序的时候,那么第二个for循环内部的swap函数就不会产生作用,那么就以标志整个冒泡排序已经可以结束了。因此使用一个flag标志来确定排序过程中已经有序的时候就应该结束,而不是继续进行比较浪费时间和资源。
选择排序
个人认为其思想其实与冒泡有些类似,冒泡是将(最大或者最小)数沉底,而选择则是将(最大或者最小)数选择出来,从数组开始位置放置。只是没有每次对数据进行交换,而是记录其下标,最后只是交换一次数据。
1 /** 2 * 选择排序 3 * 4 * @param arr 5 */ 6 public void selectSort(int[] arr) { 7 int len = arr.length; 8 int min = 0; 9 int i = 0; 10 while (i < len - 1) { 11 min = i; 12 for (int j = i + 1; j < len; j++) { 13 min = arr[min] <= arr[j] ? min : j; 14 } 15 swap(arr, i, min); 16 i++; 17 } 18 }
因此选择排序,那么就是选择数据的下标,然后进行一次交换。
如果每次进行交换数据的话,那么可以很简单的成为冒泡的一种另类写法,将沉底变成浮顶。
快速排序
感觉这是面试过程中经常让撸的一个排序算法。个人认为有以下几个原因:
- 原理简单:找一个标志,小的放左边,大的放右边。
- 直接考察算法是否具有稳定性:具有稳定性,当数据相等的时候,不进行交换,那么最后的排序结果依然是具有稳定性的,这个可以是在代码中确定。
- 代码强度:相比冒泡要复杂一些,但是比起归并和堆排序,有要简单一些,并且是平时中使用的较多的排序算法。
- 考察了递归:在实现的时候,使用递归的书写方式,可以减少代码的行数。
- 思想中具有分治的思想:归并是分治+归并,而这个算法对于归并没有那么强的要求(主要是指代码上的直接体现),那么手撸代码的时间上可以快一些。
- 代码真的很简洁。。。。
1 /** 2 * 快速排序 3 * 个人愿称它为排序中最强(狗头,概念理解容易,代码相对简洁,效率也很不错) 4 * 5 * @param arr 6 */ 7 public void quickSort(int[] arr) { 8 quickSort(arr, 0, arr.length - 1); 9 } 10 11 public void quickSort(int[] arr, int leftIndex, int rightIndex) { 12 int l = leftIndex; 13 int r = rightIndex; 14 if (l >= r) { 15 return; 16 } 17 int key = arr[l]; 18 while (l < r) { 19 while (r > l && arr[r] >= key) { 20 r--; 21 } 22 arr[l] = arr[r]; 23 while (l < r && arr[l] <= key) { 24 l++; 25 } 26 arr[r] = arr[l]; 27 } 28 arr[l] = key; 29 //printArr(arr); 30 quickSort(arr, leftIndex, l); 31 quickSort(arr, l + 1, rightIndex); 32 }
在代码实现上,关于key值的选择有各种不同,之前看到过一次左神关于快排的讲解,唯一记得就是开始(算法第一选择的key)好像是选择的最后一个数,好像是可以减少交换的次数还是怎样的,感兴趣的可以找一找。但是自己还是熟悉自己最开始理解的方式。
插入排序
关键点是:从开始算起,到任何一个数的时候,那么该数前面的数据都已经有序,因此将该数插入到它再排序中的位置,而比这个数大的都需要进行向后移动一位,因此有着【arr[j]=arr[j-1]】到最后【arr[j-2]=temp】的操作,这就是一趟插入,只是随着排序越到后面,当遇到较小的数据的时候,那么移动的数据会很多。【最坏的情况是讲一个降序的数组 使用插入排序 成为一个升序的数组】。
1 /** 2 * 插入排序 3 * 4 * @param arr 5 */ 6 public void insertSort(int[] arr) { 7 int len = arr.length; 8 for (int i = 1; i < len; i++) { 9 int temp = arr[i]; 10 int index = i; 11 for (int j = i; j > 0; j--) { 12 if (arr[j - 1] > temp) { 13 arr[j] = arr[j - 1]; 14 index = j - 1; 15 } 16 } 17 arr[index] = temp; 18 } 19 }
因此代码中的关键点是:
- 记录下当前的值。
- 移动完成之后,将当前的值放到对应的位置。
希尔排序
我自己使用得不多,不管是在哪一种地方,都没有使用多少。嗯,又是一个分治归并思想的算法吧。
嗯嗯,不想记,不想理解,不想多写。
1 /** 2 * 希尔排序 3 * 4 * @param arr 5 */ 6 public void shellSort(int[] arr) { 7 int gap = 1; 8 while (gap < arr.length) { 9 gap = gap * 3 + 1; 10 } 11 while (gap > 0) { 12 for (int i = gap; i < arr.length; i++) { 13 int tmp = arr[i]; 14 int j = i - gap; 15 while (j >= 0 && arr[j] > tmp) { 16 arr[j + gap] = arr[j]; 17 j -= gap; 18 } 19 arr[j + gap] = tmp; 20 } 21 gap /= 3; 22 } 23 }
归并排序
分治与归并的结合。重点是将每次排序结果进行归并,因此最重要的是归并函数。
好像也用到的不是很多,但是这个思想很重要,很多时候都要用到分治再归并的方式解决问题。
1 /** 2 * 归并排序 3 * 4 * @param arr 5 */ 6 public void mergeSort(int[] arr) { 7 mergeSort(arr, 0, arr.length - 1); 8 } 9 public void mergeSort(int[] arr, int leftIndex, int rightIndex) { 10 int l = leftIndex; 11 int r = rightIndex; 12 if (l >= r) { 13 return; 14 } 15 int m = l + (r - l) / 2;//可以防止越界 16 mergeSort(arr, l, m); 17 mergeSort(arr, m + 1, r); 18 mergeArr(arr, l, m, r); 19 } 20 //归并函数 21 public void mergeArr(int[] arr, int leftIndex, int midIndex, int rightIndex) { 22 int len = rightIndex - leftIndex + 1; 23 int[] arrTemp = new int[len]; 24 int l = leftIndex, m = midIndex, r = rightIndex; 25 int k = 0; 26 int i = l, j = m + 1; 27 while (i <= m && j <= r) { 28 if (arr[i] < arr[j]) { 29 arrTemp[k++] = arr[i++]; 30 } else { 31 arrTemp[k++] = arr[j++]; 32 } 33 } 34 while (i <= m) { 35 arrTemp[k++] = arr[i++]; 36 } 37 while (j <= r) { 38 arrTemp[k++] = arr[j++]; 39 } 40 for (int t = 0; t < arrTemp.length; t++) { 41 arr[leftIndex + t] = arrTemp[t]; 42 } 43 }
堆排序
其思想是完全二叉树树,在数组中,父节点与左右孩子节点之间的下标之间存在如下关系:父节点下标为【i】,那么左孩子下节点标为【2*i+1】,右孩子节点下标为【2*i+2】。
在排序之前第一步是需要建堆,根据需求建立大顶堆或者建立小顶堆,大顶堆用于升序排序,小顶堆用于降序排序;因为在排序的时候,将堆顶元素放于数组的最后一个位置,然后调节堆,然后再进行排序。
而堆的要求则是:如果是大顶堆,那么父节点的值要大于或等于左右孩子节点的值;如果是小顶堆,那么父节点的值要小于或等于左右孩子节点的值。
1 /** 2 * 堆排序 3 * 1. 建堆——大堆或小堆,大堆升序(规范化中,是将堆顶元素与堆尾元素进行交换,那么最大元素就去了数组尾部),小堆降序 4 * 2. 将堆规范化 5 * 6 * @param arr 7 */ 8 public void heapSort(int[] arr) { 9 int len = arr.length; 10 11 //大顶堆 升序 12 buildMaxHeap(arr, len); 13 for (int i = len - 1; i >= 0; i--) { 14 swap(arr, 0, i); 15 len--;//最后一个元素放置好之后就不再对该元素在堆中进行调整 16 heapify(arr, 0, len); 17 } 18 19 //小顶堆 降序 20 //buildMinHeap(arr, len); 21 ////printArr(arr); 22 //for (int i = len - 1; i >= 0; i--) { 23 // swap(arr, 0, i); 24 // len--; 25 // heapifyMin(arr, 0, len); 26 //} 27 } 28 29 //建大堆 30 public void buildMaxHeap(int[] arr, int len) { 31 for (int i = len / 2; i >= 0; i--) { 32 heapify(arr, i, len); 33 } 34 } 35 36 //建小堆 37 public void buildMinHeap(int[] arr, int len) { 38 for (int i = len / 2; i >= 0; i--) { 39 heapifyMin(arr, i, len); 40 } 41 } 42 43 //用于建大堆 44 public void heapify(int[] arr, int i, int len) { 45 //以i节点为根节点,求i节点的左右孩子节点 46 int l = 2 * i + 1; 47 int r = 2 * i + 2; 48 int max = i; 49 50 //建大堆的关键点 51 if (l < len && arr[l] > arr[max]) { 52 max = l; 53 } 54 if (r < len && arr[r] > arr[max]) { 55 max = r; 56 } 57 if (max != i) { 58 swap(arr, i, max); 59 heapify(arr, max, len); 60 } 61 } 62 63 //用于建小堆 64 public void heapifyMin(int[] arr, int i, int len) { 65 //以i节点为根节点,求i节点的左右孩子节点 66 int l = 2 * i + 1; 67 int r = 2 * i + 2; 68 int min = i; 69 70 //建小堆的关键点 71 if (l < len && arr[l] < arr[min]) { 72 min = l; 73 } 74 if (r < len && arr[r] < arr[min]) { 75 min = r; 76 } 77 if (min != i) { 78 swap(arr, i, min); 79 heapifyMin(arr, min, len); 80 } 81 }
因此在代码的实现过程中比较麻烦。
- 写出堆调整【heapify】函数。
- 写出建堆【buildHeap】函数。
- 堆排序函数,其中一直排序,一直调整堆。
嗯,以上只是根据个人对几个排序算法的理解然后写出来的一些笔记和想法,如果有不准确的地方,还望告知。