算法是由控制结构(顺序、分支和循环3种)和原操作(指固有数据类型的操作)构成的,算法时间取决于两者的综合效果。对于算法,怎样去衡量一个算法的性能好坏,这里,我们需要先了解几个指标的概念:时间频度、时间复杂度、空间复杂度、稳定性。
- 时间频度: 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
- 时间复杂度: 在刚才提到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。 一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
- 空间复杂度:指算法在运行过程中临时占用存储空间大小的量度,对不同的数据结构来说,其空间复杂度表示函数是不一致的。
- 稳定性:排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。常见算法的稳定性:堆排序、快速排序、希尔排序、直接选择排序不是稳定的排序算法,而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。稳定性的意义在于:对于排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法,例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。否则在其他情况下稳定性毫无意义。
1. 冒泡排序(BubbleSort)
原理:比较相邻的两个元素,将值大的元素放在右边。
思路:一次比较前后相邻的两个元素,将小的元素放在前面,大的元素放在后面。即:
第一轮:第1个和第2个元素进行比较,满足前面元素比后面大的,就调换位置;接着第2个和第3个元素进行比较,同样满足条件就调换位置,如此继续,直到比较到最后1个元素,第一轮比较结束,得到最后一个位置是最大数。
第二轮:第1个和第2个元素进行比较,满足前面元素比后面大的,就调换位置;接着第2个和第3个元素进行比较,同样满足条件就调换位置,如此继续,直到比较到倒数第2个元素,第二轮比较结束,得到倒数第二个位置是第二大数。
。。。。。。
经过n-1轮,即得到从小到大的有序数组。
以上思路对于有序数组来说,只需比较完第一轮就可以确定,无需进行剩下n-2轮比较。
由以上思路可知,对于n个元素的数组,采用冒泡排序,最坏情况下(数组反序),其排序次数为 ƒ(n) = $ sum_{1}^{n-1}i $ = $frac{n^{2}-n}{2}$,即时间复杂度记为O($n^{2}$);最好情况(数组顺序),其排序次数为ƒ(n) = n-1,即时间复杂度为O(n)。由于只需要一个临时空间来存储数组,因此空间复杂度为O(1)。同时冒泡也满足稳定性。
其实现代码如下:
public class BubbleSort { public static void sort(int[] nums) { int k = 0; for (int i = 0; i < nums.length - 1; i++) { boolean flag = true; for (int j = 0; j < nums.length - i - 1; j++) { if (nums[j] > nums[j + 1]) { int t = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = t; flag = false; } k++; } if (flag) break; } System.out.println("最终排序的次数:" + k); } }
2. 选择排序(SelectSort)
原理:将每个元素依次同该元素位置后排所有元素比较,值大的放在后边。
思路:
第一轮:将第1个元素和第2个元素进行比较,满足前面元素比后面大的,就调换位置;接着将第1个元素和第3个元素进行比较,同样满足条件的调换位置,如此继续,直到第1个元素与最后一个元素比较完,第一轮结束,此时第一个位置是最小值。
第二轮:将第2个元素和第3个元素进行比较,满足前面元素比后面大的,就调换位置;接着将第2个元素和第4个元素进行比较,同样满足条件的调换位置,如此继续,直到第2个元素与最后一个元素比较完,第一轮结束,此时第二个位置是倒数第二小值。
。。。。。。
经过n-1轮,即得到从小到大的有序数组。
由以上思路可知,对于n个元素的数组,采用选择排序,不管初始数组是否已经顺序,其排序次数全部为 ƒ(n) = $ sum_{1}^{n-1}i $ = $frac{n^{2}-n}{2}$,即时间复杂度记为O($n^{2}$)。同冒泡排序一样,空间复杂度为O(1)。由于存在相等的两个元数其在序列的前后位置顺序和排序后它们两个的前后位置顺序不相同,因此选择排序是不稳定的。
其实现代码如下:
public class SelectionSort { public static void sort(int[] nums) { int k=0; for (int i = 0; i < nums.length - 1; i++) { for (int j = i + 1; j < nums.length; j++) { if (nums[i] > nums[j]) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } k++; } } System.out.println("排序总次数:"+k); } }
3. 直接插入排序(InsertSort)
原理:将数组分为两部分,前面部分为有序数组和后面部分为无序数组,其中无序数组中每个元素分别依次与其位置前的有序数组元素进行比较,当某个位置的无序元素小于该位置前面1个或多个有序元素时,有序元素依次向后移动一位,直到该无序元素开始大于或 等于有序数组元素时,标记下此时该有序元素的位置,并将该无序元素插入其位置后一位。
思路:
第一轮:将数组第1个元素看作是有序数组的唯一元素,将数组第2个元素与第1个元素进行比较,得到前2位是一个有序数组。
第二轮:将第一轮得到的有序数组,即前两位为有序元素,数组第3个元素与有序数组进行比较,得到前3位是一个有序数组。
。。。。。。
经过n-1轮,即得到从小到大的有序数组。
由以上思路可知,对于n个元素的数组,采用直接插入排序,最坏情况下(数组反序),其排序次数为 ƒ(n) = $ sum_{1}^{n-1}i $ = $frac{n^{2}-n}{2}$,即时间复杂度记为O($n^{2}$);最好情况(数组顺序),其排序次数为ƒ(n) = n-1,即时间复杂度为O(n)。由于只需要一个临时空间来存储数组,因此空间复杂度为O(1)。同时直接插入排序也满足稳定性。
直接插入排序对于小规模数据或者基本有序时十分高效。
其实现代码如下:
public class InsertSort { public static void sort(int[] nums) { int k = 0;//计算比较次数 for (int i = 1; i < nums.length; i++) {//无序元素 int t = nums[i]; int j; for (j = i - 1; j >= 0 && nums[j] > t; j--) {//有序元素 nums[j+1] = nums[j]; k++; } nums[j+1] = t; k++; } System.out.println("排序总次数:" + k); } }
4.希尔排序(ShellSort)
原理:希尔排序也称为“缩小增量排序”,将数组元素分解为多个子列表,每个子列表进行直接插入排序。
思路:
1.分割增量:采用二分法将待排序列分成两子部分,以增量分界点,前面一部分子序列作为有序序列,后面一部分子序列(包括增量分界点)作为无序序列;
2.将上面两个子序列采用直接插入排序算法进行排序;
3.继续分割增量,重复上述操作,直至增量为1。
希尔排序是对直接插入排序的一种优化,小规模数据或者基本序列基本有序不常见,而希尔排序针对直接插入排序进行的优化。直接插入排序的时间复杂度是O($n^{1.3-2}$),希尔排序没有快速排序算法快 O(nlogn),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(n^2)复杂度的算法快得多。
1 import java.util.Arrays; 2 import java.util.Objects; 3 4 /** 5 * 希尔排序:也称为“缩小增量排序”,其基本思想是将数组元素分解为多个子列表,每个列表进行直接插入排序 6 */ 7 public class ShellSort { 8 9 public static void checkNums(int[] nums) { 10 if (Objects.isNull(nums) || nums.length == 0) { 11 throw new IllegalArgumentException("数组异常"); 12 } 13 } 14 15 public static void shellSort(int[] nums) { 16 int N = nums.length; 17 // 进行分组,最开始的增量(gap)为数组长度的一半 18 for (int gap = N / 2; gap > 0; gap /= 2) { 19 for (int i = gap; i < N; i++) { 20 int t = nums[i]; 21 int j; 22 for (j = i - 1; j >= 0 && nums[j] >= t; j--) { 23 nums[j + 1] = nums[j]; 24 } 25 nums[j + 1] = t; 26 } 27 } 28 } 29 }
5.快速排序(QuickSort)
原理:快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分数据均比另一部分数据小。之后分别对这两部分数据继续进行排序,以达到整个序列有序的目的。
思路:快速排序分三个步骤:
- 选择基准:在待排序列中,按照某种方式选出某个元素,作为“基准值”(pivot);
- 分割操作:以该基准值在序列中实际位置,把序列分为两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比该基准大;
- 递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。
快排最坏的情况下,即在选取基准值为最左边或最右边元素时,当出现 1)数组正序;2)数组反序;3)数组元素全部相同,此时快排可能退化成时间复杂度为O($n^{2}$),最好情况或者平均情况,时间复杂度为O(nlogn)。
下面以“固定基准”法来实现:
1 import java.util.Arrays; 2 import java.util.Objects; 3 4 /** 5 * 快速排序:运用了分治思想,取某个基准点值,将数组依次从左-->右进行遍历(寻找直到大于基准点的数)和从右-->左进行遍历(寻找直到小于基准点的数), 6 * 然后交换位置,使得左边的元素小于基准点数值,右边元素大于基准点数值 7 */ 8 public class QuickSort { 9 10 public static void checkNums(int[] nums) { 11 if (Objects.isNull(nums) || nums.length == 0) { 12 throw new IllegalArgumentException("数组异常"); 13 } 14 } 15 16 public static void quickSort(int[] nums, int left, int right) { 17 // 递归出口 18 if (left >= right) return; 19 // 选中第一个元素作为基准值 20 int pivot = nums[left]; 21 int i = left; 22 int j = right; 23 System.out.println("left=" + left + ", right=" + right); 24 while (i < j) { 25 while (nums[i] < pivot && i < j) i++; 26 while (nums[j] > pivot && i < j) j--; 27 if (i < j) { 28 swap(nums, i, j); 29 } 30 System.out.println(Arrays.toString(nums)); 31 } 32 System.out.println("i=" + i + ", j=" + j); 33 System.out.println(Arrays.toString(nums)); 34 quickSort(nums, left, i - 1); 35 quickSort(nums, i + 1, right); 36 } 37 38 public static void swap(int[] nums, int i, int j) { 39 nums[i] ^= nums[j]; 40 nums[j] ^= nums[i]; 41 nums[i] ^= nums[j]; 42 } 43 }
上述代码对于无重复元素的数组,可以实现满足,但是如果对于重复的,该代码会存在重大的bug,程序执行会陷入死循环中,因此,对于重复元素最多只重复一次的,正确解法如下:
1 import java.util.Arrays; 2 import java.util.Objects; 3 4 /** 5 * 快速排序:运用了分治思想,取某个基准点值,将数组依次从左-->右进行遍历(寻找直到大于基准点的数)和从右-->左进行遍历(寻找直到小于基准点的数), 6 * 然后交换位置,使得左边的元素小于基准点数值,右边元素大于基准点数值 7 */ 8 public class QuickSort { 9 10 public static void checkNums(int[] nums) { 11 if (Objects.isNull(nums) || nums.length == 0) { 12 throw new IllegalArgumentException("数组异常"); 13 } 14 } 15 16 public static void quickSort(int[] nums, int left, int right) { 17 // 递归出口 18 if (left >= right) return; 19 // 选中第一个元素作为基准值 20 int pivot = nums[left]; 21 int i = left; 22 int j = right; 23 System.out.println("left=" + left + ", right=" + right); 24 while (i < j) { 25 while (nums[i] < pivot && i < j) i++; 26 while (nums[j] > pivot && i < j) j--; 27 if (nums[i] == nums[j]) break; 28 if (i < j) { 29 swap(nums, i, j); 30 } 31 System.out.println(Arrays.toString(nums)); 32 } 33 System.out.println("i=" + i + ", j=" + j); 34 System.out.println(Arrays.toString(nums)); 35 quickSort(nums, left, i - 1); 36 quickSort(nums, i + 1, right); 37 } 38 39 public static void swap(int[] nums, int i, int j) { 40 nums[i] ^= nums[j]; 41 nums[j] ^= nums[i]; 42 nums[i] ^= nums[j]; 43 } 44 }
如果同一元素重复超过两次,即出现3次以上,那么上诉代码仍然不能正确排序,继续迭代算法:
1 import java.util.Arrays; 2 import java.util.Objects; 3 4 /** 5 * 快速排序:运用了分治思想,取某个基准点值,将数组依次从左-->右进行遍历(寻找直到大于基准点的数)和从右-->左进行遍历(寻找直到小于基准点的数), 6 * 然后交换位置,使得左边的元素小于基准点数值,右边元素大于基准点数值 7 */ 8 public class QuickSort { 9 10 public static void checkNums(int[] nums) { 11 if (Objects.isNull(nums) || nums.length == 0) { 12 throw new IllegalArgumentException("数组异常"); 13 } 14 } 15 16 public static void quickSort(int[] nums, int left, int right) { 17 // 递归出口 18 if (left >= right) return; 19 // 选中第一个元素作为基准值 20 int pivot = nums[left]; 21 int i = left; 22 int j = right; 23 System.out.println("left=" + left + ", right=" + right); 24 while (i < j) { 25 while (nums[i] < pivot && i < j) i++; 26 while (nums[j] > pivot && i < j) j--; 27 if (nums[i] == nums[j]) j--; 28 if (i < j) { 29 swap(nums, i, j); 30 } 31 System.out.println(Arrays.toString(nums)); 32 } 33 System.out.println("i=" + i + ", j=" + j); 34 System.out.println(Arrays.toString(nums)); 35 quickSort(nums, left, i - 1); 36 quickSort(nums, i + 1, right); 37 } 38 39 public static void swap(int[] nums, int i, int j) { 40 nums[i] ^= nums[j]; 41 nums[j] ^= nums[i]; 42 nums[i] ^= nums[j]; 43 } 44 }
快速排序的基准选择存在三种:固定基准、随机基准、三数取中,以及快排的优化,后面会更新这块的博客。
6.归并排序(MergeSort)
原理:归并排序使用了分治思想,将待排序列拆分为诺干个子序列,直到每个子序列都只有一个元素,然后逐步排序并合并。归并排序也称为二路合并。
思路:归并排序分为三个步骤:
1.递归二分法拆分两个子序列,直至子序列的元素为1;
2.申请临时空间序列,比较子序列元素,将排好序的元素放入临时空间中;
3.将临时空间序列的元素放到原序列对应的位置。
归并排序是稳定排序。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。每次合并都需要临时空间,因此排序的空间复杂度为O(n)。总的平均时间复杂度为O($nlog_{2}n$)。而且,归并排序的最好,最坏,平均时间复杂度均为O($nlog_{2}n$)。
1 import java.util.Arrays; 2 import java.util.Objects; 3 4 /** 5 * 归并排序:分治思想(分:以二分法无限将原序列拆分为两个子序列,直至子序列的元素为1;治:将子序列依次合并并使其有序) 6 * 步骤: 7 * 1.递归二分法拆分两个子序列,直至子序列的元素为1; 8 * 2.申请临时空间序列,比较子序列元素,将排好序的元素放入临时空间中 9 * 3.将临时空间序列的元素放到原序列对应的位置 10 */ 11 public class MergeSort { 12 13 public static void checkNums(int[] nums) { 14 if (Objects.isNull(nums) || nums.length == 0) { 15 throw new IllegalArgumentException("数组异常"); 16 } 17 } 18 19 // 拆分直至子序列元素为1 20 public static void divide(int[] nums, int L, int R) { 21 if (L == R) return; 22 int mid = ((R - L) >> 1) + L; 23 divide(nums, L, mid); 24 divide(nums, mid + 1, R); 25 mergeSort(nums, L, mid, R); 26 } 27 28 public static void mergeSort(int[] nums, int L, int mid, int R) { 29 // 申请临时空间 30 int[] temp = new int[R - L + 1]; 31 int i = L; 32 int j = mid + 1; 33 int k = 0; 34 // 比较子序列的元素,哪个小就放入临时数组中 35 while (i <= mid && j <= R) { 36 temp[k++] = nums[i] < nums[j] ? nums[i++] : nums[j++]; 37 } 38 // 循环退出后,把剩下元素放入临时数组中,以下两while循环只会执行其中一个 39 while (i <= mid) { 40 temp[k++] = nums[i++]; 41 } 42 while (j<= R) { 43 temp[k++] = nums[j++]; 44 } 45 System.out.println("临时数组temp:" + Arrays.toString(temp)); 46 // 将临时数组复制到原数组对应的位置 47 System.arraycopy(temp, 0, nums, L, temp.length); 48 System.out.println("排序中:" + Arrays.toString(nums)); 49 } 50 }
7.堆排序(HeapSort)
原理:利用完全二叉树的结构进行选择排序,排完序后每个节点值大于左右孩子节点值,称为大顶堆,相反每个节点值小于左右孩子节点值,称为小顶堆。
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
父节点的下标 =(子节点的下标-1)>> 1
左子节点的下标 = 父节点的下标 * 2 + 1
右子节点的下标 = 父节点的下标 * 2 + 2
思路:
1.构造初始堆,使得堆顶节点值最大(大顶堆),构造原则是从左往右,从下往上;
2.将顶端与末尾进行交互,此时得到序列最后一个元素是最大值,数组序列剩下n-1个元素待排序;
3.将数组剩下n-1个元素构造大顶堆,再将其与数组n-1位置元素交换,如此重复,直到数组有序
堆排序是不稳定排序,同归并排序一样,最好、最坏情况下,以及平均时间复杂度均为O($nlog_{2}n$)。
1 import java.util.Arrays; 2 import java.util.Objects; 3 4 /** 5 * 堆排序:完全二叉树结构,采用选择排序 6 * <p> 7 * 父节点的下标 =(子节点的下标-1)>> 1 8 * 左子节点的下标 = 父节点的下标 * 2 + 1 9 * 右子节点的下标 = 父节点的下标 * 2 + 2 10 */ 11 public class HeapSort { 12 13 public static void checkNums(int[] nums) { 14 if (Objects.isNull(nums) || nums.length == 0) { 15 throw new IllegalArgumentException("数组异常"); 16 } 17 } 18 19 public static void buildMaxHeap(int[] nums, int heapSize) { 20 // 递归出口 21 if (heapSize == 1) return; 22 // 找到末尾非叶子节点,并进行比较排序,得到堆顶元素为最大 23 for (int i = (heapSize >> 1) - 1; i >= 0; i--) { 24 heapSort(nums, i, heapSize); 25 } 26 // 交换堆顶与堆尾值 27 swap(nums, 0, heapSize - 1); 28 buildMaxHeap(nums, --heapSize); 29 } 30 31 public static void heapSort(int[] nums, int i, int heapSize) { 32 // 左子节点 33 int left = 2 * i + 1; 34 // 右子节点 35 int right = 2 * i + 2; 36 37 // 比较当前节点与左子节点大小 38 if (nums[i] < nums[left]) { 39 swap(nums, i, left); 40 } 41 // 判断是否存在右子节点,存在,则比较节点与右子节点大小 42 if (right < heapSize && nums[i] < nums[right]) { 43 swap(nums, i, right); 44 } 45 46 } 47 48 public static void swap(int[] nums, int i, int j) { 49 nums[i] ^= nums[j]; 50 nums[j] ^= nums[i]; 51 nums[i] ^= nums[j]; 52 } 53 54 public static void main(String[] args) { 55 int[] nums = new int[10]; 56 for(int i=0;i<nums.length;i++){ 57 nums[i] = (int)(Math.random()*10); 58 } 59 System.out.println("排序前:" + Arrays.toString(nums)); 60 checkNums(nums); 61 buildMaxHeap(nums, nums.length); 62 System.out.println("排序后:" + Arrays.toString(nums)); 63 } 64 }
8.计数排序(CountingSort)
原理:计数排序是一种非比较排序,通过计数元素值出现的次数来标记,从而达到顺序。
思路:
- 找出原数组中元素的最大值,记为max;
- 创建一个新的标记数组,数组大小为max+1,遍历原数组,以元素的值作为新数组的下标,出现次数为标记数组的值;
- 遍历标记数组,将其元素值不为0的下标依次填充到原数组中。
计数排序是稳定的排序(两个相同的元素依次统计到标记数组中,标记数组储存按照先进先出的原则填充到原数组中),其时间复杂度和空间复杂度均为O(n+k)
1 import java.util.Arrays; 2 import java.util.Objects; 3 4 /** 5 * 计数排序:非比较的排序算法,通过统计数组元素出现的次数,然后依次遍历标记数组,将其下标填充到原数组中,即完成排序 6 * 计数排序是稳定的排序,其时间和空间复杂度均为O(n+k) 7 */ 8 public class CountingSort { 9 10 public static void checkNums(int[] nums) { 11 if (Objects.isNull(nums) || nums.length == 0) { 12 throw new IllegalArgumentException("数组异常"); 13 } 14 } 15 16 public static void countingSort(int[] nums) { 17 if (nums.length == 1) return; 18 int max = nums[0]; 19 // 获取原数组最大元素值 20 for (int i = 1; i < nums.length; i++) { 21 max = max - nums[i] > 0 ? max : nums[i]; 22 } 23 // 创建大小为max+1的标记数组 24 int[] countNums = new int[max + 1]; 25 for (int num : nums) { 26 countNums[num]++; 27 } 28 // 遍历标记数组,将其下标填充入原数组中 29 int index = 0; 30 for (int i = 0; i < max + 1; i++) { 31 for (int j = 0; j < countNums[i]; j++) { 32 nums[index++] = i; 33 } 34 } 35 } 36 37 public static void main(String[] args) { 38 int[] nums = new int[10]; 39 for (int i = 0; i < nums.length; i++) { 40 nums[i] = (int) (Math.random() * 10); 41 } 42 System.out.println("排序前:" + Arrays.toString(nums)); 43 checkNums(nums); 44 countingSort(nums); 45 System.out.println("排序后:" + Arrays.toString(nums)); 46 } 47 }
如果待排序的数组不是从1开始,比如待排序数组【32,39,36,35,31,33,37,38】,此时就会存在标记数组空间浪费问题,即需要优化,优化思路:
- 找出原数组中元素的最大值max和最小值min;
- 创建一个新的标记数组,数组大小为len(len取值规则:max-min+1和nums.lenth,取其大的值),遍历原数组,以元素的值作为新数组的下标,出现次数为标记数组的值;
- 遍历标记数组,将其元素值不为0的下标依次填充到原数组中。
1 import java.util.Arrays; 2 import java.util.Objects; 3 4 /** 5 * 计数排序:非比较的排序算法,通过统计数组元素出现的次数,然后依次遍历标记数组,将其下标填充到原数组中,即完成排序 6 * 计数排序是稳定的排序,其时间和空间复杂度均为O(n+k) 7 */ 8 public class CountingSort { 9 10 public static void checkNums(int[] nums) { 11 if (Objects.isNull(nums) || nums.length == 0) { 12 throw new IllegalArgumentException("数组异常"); 13 } 14 } 15 16 public static void countingSort(int[] nums) { 17 if (nums.length == 1) return; 18 int max = nums[0]; 19 int min = nums[0]; 20 21 // 获取原数组最大元素值 22 for (int i = 1; i < nums.length; i++) { 23 max = max - nums[i] > 0 ? max : nums[i]; 24 min = min - nums[i] < 0 ? min : nums[i]; 25 } 26 // 创建大小为len的标记数组,len取值规则:max-min+1和nums.lenth,取其大的值 27 int len = max - min + 1 - nums.length >= 0 ? max - min + 1 : nums.length; 28 int[] countNums = new int[len]; 29 for (int num : nums) { 30 countNums[num]++; 31 } 32 // 遍历标记数组,将其下标填充入原数组中 33 int index = 0; 34 for (int i = 0; i < len; i++) { 35 for (int j = 0; j < countNums[i]; j++) { 36 nums[index++] = i; 37 } 38 } 39 } 40 41 public static void main(String[] args) { 42 int[] nums = new int[10]; 43 for (int i = 0; i < nums.length; i++) { 44 nums[i] = (int) (Math.random() * 10); 45 } 46 System.out.println("排序前:" + Arrays.toString(nums)); 47 checkNums(nums); 48 countingSort(nums); 49 System.out.println("排序后:" + Arrays.toString(nums)); 50 } 51 }
9.桶排序 (BucketSort)
原理:桶排序是计数排序的升级版,解决小数排序问题。将数组分别纳入k个桶中,每个桶的步长为数组的(maxValue-minValue)/k,然后对每个桶进行快排或归并排序。
思路:
- 求待排数组最大值和最小值;
- 初始化桶为k;
- 遍历装桶;
- 桶内排序;
- 结果输出
桶排序最好的情况是,数组均匀分布在每个桶中,最坏情况是分布在一个桶中。时间复杂度:第1步的时间复杂度为n;第2步的时间复杂度为k;第3步的时间复杂度为n;
第4步的时间复杂度k*(n/k)*log(n/k)=n*log(n/k);第5步的时间复杂度为n;因此时间复杂度为n+k+n+n*log(n/k)+n=3n+k+n*log(n/k)=O(n+k);空间复杂度为:O(n+k),空间最好情况下对桶采用链表存储,但链表排序对时间复杂要求高,而采用数组则会牺牲更多的空间。因此桶排序是存在瑕疵的排序。桶排序只做了解,该排序的代码本期不作实现。
10.基数排序(RadixSort)
原理:基数排序是一种非比较类型的整数排序,将整数按位数切割成不同的数字,然后按每个位数分别排序。
思路:
基数排序分为最低位优先法(LSD)和最高位优先法(MSD),以LSD为实现方式:
- 以待排数组元素最大为基准,获取数组最大位数,同时将数组统一为位数相同的整数,高位不足补0(这里只考虑非负整数);
- 从最低位开始,按照每位数字从小到大,统计到0-9共10个元素的桶中,数组下标对应该数字,数值对应出现次数;
- 将桶的数值变成待排数组元素的位置索引;
- 根据桶的元素数值,遍历待排数组,将其依次放入临时数组中;
- 将临时数组拷贝到待排数组;
- 重复上述步骤,直到高位,即得到整个数组有序
基数排序的时间复杂度为:O(d*(n+radix));空间复杂度为:O(n+d*(n+radix))【n:待排元素的个数,d:待排数组元素最大值的位数,radix:桶的大小,这里分0-9十个桶】
1 public class RadixSort { 2 3 /** 4 * 检查数组 5 * @param nums 待排序数组 6 */ 7 public static void checkNums(int[] nums) { 8 if (Objects.isNull(nums) || nums.length == 0) { 9 throw new IllegalArgumentException("数组异常"); 10 } 11 } 12 13 /** 14 * 获取数组最大数的位数 15 * @param nums 待排数组 16 * @return 数组最大值位数 17 */ 18 public static int getMaxLength(int[] nums) { 19 int max = nums[0]; 20 for (int i = 1; i < nums.length; i++) { 21 max = max - nums[i] >= 0 ? max : nums[i]; 22 } 23 char[] maxChar = String.valueOf(max).toCharArray(); 24 return maxChar.length; 25 } 26 27 /** 28 * 获取元素指定位数的数字 29 * @param num 元素值 30 * @param d 位数 31 * @return 元素指定位置的值 32 */ 33 public static int getDigitValue(int num, int d) { 34 return num % (new Double(Math.pow(10d, d)).intValue()) / (new Double(Math.pow(10d, d - 1)).intValue()); 35 } 36 37 /** 38 * 基数排序 39 * @param nums 待排数组 40 * @param digit 待排数组排序的位数 41 * @param maxLength 待排数组元素的最大位数 42 */ 43 public static void radixSort(int[] nums, int digit, int maxLength) { 44 // 递归出口 45 if (digit > maxLength) return; 46 // 桶 47 int len = 10; 48 int[] bucket = new int[len]; 49 // 临时数组 50 int[] temp = new int[nums.length]; 51 for (int num : nums) { 52 bucket[getDigitValue(num, digit)]++; 53 } 54 // 将桶的元素值变成待排元素的位置索引 55 for (int i = 1; i < len; i++) { 56 bucket[i] = bucket[i] + bucket[i - 1]; 57 } 58 System.out.println(Arrays.toString(bucket)); 59 for (int i = nums.length - 1; i >= 0; i--) { 60 int d = getDigitValue(nums[i], digit); 61 temp[bucket[d] - 1] = nums[i]; 62 --bucket[d]; 63 } 64 System.arraycopy(temp, 0, nums, 0, temp.length); 65 radixSort(nums, digit + 1, maxLength); 66 } 67 68 public static void main(String[] args) { 69 int[] nums = new int[10]; 70 for (int i = 0; i < nums.length; i++) { 71 nums[i] = (int) (Math.random() * 1000); 72 } 73 System.out.println("排序前:" + Arrays.toString(nums)); 74 checkNums(nums); 75 int maxLength = getMaxLength(nums); 76 radixSort(nums, 1, maxLength); 77 System.out.println("排序后:" + Arrays.toString(nums)); 78 } 79 }