目录
1 排序算法的分类
排序是将一组无序的数字元素,经过重新排列后形成的一组有序的数字元素,可以是升序也可以是降序。排序算法是对数字元素排列的过程中经过的一些列运算。根据是否需要比对,可以把排序算法分为 比较类排序算法 和 非比较类排序算法。
- 比较类排序算法:通过比较来确定元素之间的相对顺序。比较类排序算法的时间复杂度不能突破 O(NlogN),所以也成为非线性时间比较类排序。
- 非比较类排序算法:不通过比较来确定元素之间的相对顺序。非比较类排序算法可以突破比较类排序算法时间复杂度的下界,以线性时间运行,因此也称为线性时间非比较类排序。
常见的十大排序算法归类如下所示:
我们常用的冒泡排序、快速排序、插入排序、希尔排序、选择排序、堆排序、归并排序都属于比较类排序算法,因为要通过比较来判断元素的顺序。技术排序、桶排序、基数排序都属于非比较类排序算法。下面我们一起来拆解一下这十种排序算法。
2 十大排序算法详解
2.1 冒泡排序
2.3.1 解题思路
原理:比较两个相邻的元素,将大的元素交换到右边。
思路:依次比较两个相邻的元素,将小的元素放到左边,大的元素放到右边。
1)第一次比较:比较第一个元素和第二个元素,将小的放到左边,大的放到右边;
2)第二次比较:比较第二个元素和第三个元素,将小的放到左边,大的放到右边;
…………
3)依次比较相邻的两个元素,直到最后两个元素,把小的放到左边,大的放到右边,这样数组最后一个元素就是整个数组中最大的元素;
4)然后开始第二轮比较,第一轮得到了最后的元素,所以最后一个元素不参与比较,第二轮会得到倒数第二个元素。
5)第二轮比较完成之后,得到的倒数第二个元素一定是整个数组中第二大的元素。在第三轮比较中倒数第二个元素不参与比较。
6)依次类推,每一轮比较,元素就少一个。
2.3.2 编码实现
public static void bubbleSort(int[] nums){
for(int i=0;i<nums.length;i++){
for(int j=0;j<nums.length-1-i;j++){
if(nums[j]>nums[j+1]){
int tmp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = tmp;
}
}
}
}
2.3.3 时间复杂度和空间复杂度
冒泡排序有两层 for 循环,所以平均时间复杂度 T(n) = O(n^2) 。排序的过程中没有额外使用空间,所以空间复杂度 S(n) = O(1)。
2.2 快速排序
2.3.1 解题思路
快速排序的核心思想是找到一个目标值 key ,使用数组 nums 中的元素和 key 进行比较,大于 key 的元素放到数组的一边,小于 key 的元素放到数组的另一边,这样 key 的位置就确定了,依次使用这种方式对两边的数组进行排序,最终得到排好序的数组。
示例:
输入 nums = {4,8,1,5,9,2} 输出 nums = {1,2,4,5,8,9}
快速排序执行步骤:
初始化:nums = {4,8,1,5,9,2}
第一步:nums = {4,8,1,5,9,2}
选定最左边的元素 4 作为目标值。先从左边开始比较,8 大于 4 ,所以 leftIndex 指针停留在指向 8 的位置,值为 1。
第二步:nums = {4,2,1,5,9,8}
从右边比较,2 小于 4 ,所以 rightIndex 指针停留在指向 2 的位置,值为 5。交换 下标 0 和 5 的值。
第三步:nums = {4,2,1,5,9,8}
继续从左边开始比较,1 小于 4,leftIndex--,指针指向 5,5 大于 4,所以 leftIndex 停留在 5 的位置,leftIndex = 3 。
第四步:nums = {4,2,1,5,9,8}
继续从右边比较,8,9都大于 4 ,rightIndex最后也指向 5 。
第五步:nums = {1,2,4,5,9,8}
判断 5 大于 4 ,所以交换 leftIndex-1 位置 和 key 位置 的元素。
第六步:nums1 = {1,2}
按照第一步到第五步排序左侧数组。
第七步:nums2 = {5,8,9}
按照第一步到第五步排序右侧数组。
最终得到排序好的数组: nums = {1,2,4,5,8,9}
2.3.2 编码实现
public static void fastSort1(int[] nums,int left,int right){
if(left<right){
int mark = sort1(nums,0,nums.length-1);
fastSort(nums,left,mark-1);
fastSort(nums,mark+1,right);
}
}
public static int sort1(int[] nums,int left,int right){
int leftIndex = left+1;
int rightIndex = right;
int mark = -1;
int key = nums[left];
while(leftIndex<rightIndex){
while(nums[leftIndex]<key && leftIndex<rightIndex){
leftIndex++;
}
while(nums[rightIndex]>key && leftIndex<rightIndex){
rightIndex--;
}
if(left<right){
int tmp = nums[leftIndex];
nums[leftIndex] = nums[rightIndex];
nums[rightIndex] = tmp;
}
}
if(nums[leftIndex]<key){
mark = leftIndex;
} else {
mark = leftIndex-1;
}
nums[left] = nums[mark];
nums[mark] = key;
return mark;
}
2.3.3 时间复杂度和空间复杂度
快速排是采用分治法进行遍历的,相当于遍历一个二叉树,理想的二叉树结构如下图所示:
这种情况二叉树的深度是3,根据完全二叉树的定义,深度计算公式 log(N+1),每一层需要比较N次,所以时间复杂度是 T(n) = O(N*logN)。因为是使用的递归,所以每一层递归都需要数组存储,理想情况下空间复杂度 S(n) = O(logN)。
上面讲的是最理想的情况下,空间复杂度和时间复杂度,二叉树有可能并不是上面的样子,二叉树的深度有可能是N ,这种情况下 时间复杂度 T(n) = O(n^2) ,空间复杂度 S(n) = O(n)。
快速排序的平均时间复杂度 T(n) = O(N*logN) ,空间复杂度 S(n) = O(NlogN) 。
2.3 插入排序
2.3.1 解题思路
给定一个无序数组 nums,使用插入排序使数组中的元素按照升序或者降序排列。插入排序的核心思路是把数组 nums 一分为二,一个是已经排好序的数组 nums1,一个是无序的数组nums2,把无序数组 nums2 中的元素,插入有序数组 nums1 中,就得到了一个有序数组,插入的时候需要通过比较值的大小来判断 nums2 中的元素(carry)放在 nums1中的哪个位置。通常是从 nums1 数组的后面开始比较,carry 从后往前和 nums1 的元素做比较,找到不小于 carry 的元素,把 carry 插入该元素的后面,该元素后面的元素都向后移位。依次把 nums2 中的元素插入 nums1 中即可。
示例:
输入 nums = {4,8,1,5,9,2} 输出 nums1 = {1,2,4,5,8,9}
插入排序执行步骤:
第一步:nums1 = {} nums2 = {4,8,1,5,9,2}
第二步:nums1 = {4} nums2 = {8,1,5,9,2}
第三步:nums1 = {4,8} nums2 = {1,5,9,2}
第四步:nums1 = {1,4,8} nums2 = {5,9,2}
第五步:nums1 = {1,4,5,8} nums2 = {9,2}
第六步:nums1 = {1,4,5,8,9} nums2 = {2}
第六步:nums1 = {1,2,4,5,8,9} nums2 = {}
最终得到排序好的数组 nums= {1,2,4,5,8,9}。在编码中不会实际定义新的数组,而是在数组nums中通过移位来实现插入排序。
2.3.2 编码实现
public static void insertSort(int[] nums){
for(int i=0;i<nums.length-1;i++){
for(int j=i+1;j>0&&(nums[j]<nums[j-1]);j--){
int tmp = nums[j-1];
nums[j-1] = nums[j];
nums[j] = tmp;
}
}
}
2.3.3 时间复杂度和空间复杂度
对于随机排列的长度为N的数组,使用快速排序,最坏情况下需要 (N^2)/2 次比较, (N^2)/2 次交换。最好情况下需要 N-1 次比较,0 次交换。平均情况下需要 (N^2)/4 次比较, (N^2)/4 次交换。所以快速排序的时间复杂度 S(n) = O(n^2)。这个从编码中也可以很容易看到,有两层for循环。
快速排序整个过程都在原来的数组进行,没有额外定义数据,只定义了一个临时变量 tmp ,所以空间复杂度 T(n) = O(1) 。
2.4 希尔排序
2.3.1 解题思路
希尔排序是第一个算法复杂度突破 O(n^2) 的排序算法,是在插入排序的基础上做了改进,也称为缩小增量排序。希尔排序会把一个序列拆分成多个子序列,分别对多个子序列进行插入排序,然后在合并排好序的子序列,再进行整体的插入排序。子序列的拆分方法是按下标的一定增量来分组,对每一组使用插入排序进行排序,随着增量逐渐减少,每组包含的关键词逐渐增多,当增量减至1时,所有的元素都在同一个组,算法便终止。
希尔排序实现了跳跃式的比较和插入,相比直接插入排序,希尔排序针对已经排好序的数组有更优的性能。例如数组 {6,5,4,3,2,1,0} 要实现升序排列,使用直接插入排序算法,元素 0 要到数组的左端需要经过 n-1 次比较和 n-1 次移位。希尔排序把数组采用跳跃式分组,按照一定的增量把数组划分成若干组,例如假设第一次增量是2,那么数组被划分为2组,{6,4,2,0} 一组,{5,3,1} 一组,这个时候元素 0 要到数组最左端只需要比较 (n)/2 次,移动 (n)/2次 就可以了。随后逐步缩小增量,直至增量为 1,排序就完成了。
希尔排序的关键在于如何选择增量,增量的计算方式有很多种,这里使用希尔增量,即 gap=n/2 ,这样得到的增量序列为 {n/2 , (n/2)/2,…,1 },希尔排序的增量序列的选择和证明是个数学难题,希尔增量是比较常用的,也是希尔建议的增量,但是并不是最优的。这里不再讨论最优增量。
示例:
输入 nums = {7,5,9,2,4,6,3,0,1,8} 输出 nums = {0,1,2,3,4,5,6,7,8,9}
初始数组如下图所示:
对数组按照一定增量进行分组, gap = n/2 = 10/2 = 5。这意味着数组被分为5组 {7,6},{5,3},{9,0},{2,1},{4,8},如下图所示:
依次对分好的组使用插入排序,得到如下所示的数组:
可以看到 6,3,0,1 四个元素的位置被调前了。
然后缩小增量值 gap = 5/2 = 2,这样数组被分为2组,{6,0,4,5,2}一组,{3,1,7,9,8}一组,如下图所示:
依次对分好的两个数组使用直接插入排序,得到如下所示的数组:
然后再缩小增量值 gap = 2/2 = 1 ,这样得到最后的一个数组,其实可以看到现在的数组基本有序,只需要做微调就可以了。得到的数组如下所示:
对这个数组使用插入排序,得到最终排序号的数组:
2.3.2 编码实现
public static void shellSort(int[] nums){
int gap = nums.length/2;
while(gap>=1){
for(int i=gap;i<nums.length;i++){
for(int j=i;j>=gap&&(nums[j]<nums[j-gap]);j-=gap){
int tmp = nums[j-gap];
nums[j-gap] = nums[j];
nums[j] = tmp;
}
}
gap = gap/2;
}
}
2.3.3 时间复杂度和空间复杂度
希尔排序的时间复杂度跟增量序列有关,以希尔增量为例,希尔排序的平均时间复杂度
T(n) = O(n^(3/2))。希尔排序没有额外使用空间,所以空间复杂度为 S(n) = O(1)。
2.5 选择排序
2.5.1 解题思路
选择排序是逻辑比较简单的一种排序算法,核心思想是遍历无序的元素找到最大或者最小值,放入有序元素的末尾。所有元素遍历一遍即把所有元素完成排序。
示例:
输入 nums = {4,8,1,5,9,2} 输出 nums1 = {1,2,4,5,8,9}
插入排序执行步骤:
第一步:初始的时候整个数组都是无序的,所以假设数组最左端的元素 4 是最小值,后面的数组依次和4比较,如果有比4小的元素,交换两个元素的位置。第一轮循环得到的数组如下所示:
nums = {1,8,4,5,9,2}
第二步:然后指针指向 8 ,同样假设 8 是后面无序数组最小的元,依次进行比较,首先4和8交换位置,然后4和后续数组依次比较,最后得出第二小的元素 2。第二轮循环得到的数组如下所示:
nums = {1,2,8,5,9,4}
第三步:第三轮循环得到如下所示数组:
nums = {1,2,4,8,9,5}
第四步:第四轮循环
nums = {1,2,4,5,9,8}
第五步:第五轮循环
nums = {1,2,4,5,8,9}
2.5.2 编码实现
public static void selectSort(int[] nums){
for(int i=0;i<nums.length-1;i++){
for(int j=i+1;j<nums.length;j++){
if(nums[j]<nums[i]){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
}
}
2.5.3 时间复杂度和空间复杂度
选择排序的时间复杂度很好计算,有两层for循环,所以时间复杂度 T(n) = O(n^2)。由于排序过程都是在原数组中进行的,所有空间复杂度 S(n) = O(1)。
2.6 堆排序
2.3.1 解题思路
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序。要使用堆这种数据结构,首先要理清什么是堆?堆可以分为大顶堆和小顶堆。堆是一颗完全二叉树,每个节点的值都大于或等于左右孩子节点的值,称为大顶堆;每个节点都小于或等于左右孩子节点的值,称为小顶堆。如下图所示:
如果使用数组来表示堆,我们对堆中的节点按层次编号,如下图所示:
那么大顶堆和小顶堆的定义:
大顶堆:nums[i]>=nums[2i+1] && nums[i]>=nums[2i+2]
小顶堆: nums[i]<=nums[2i+1] && nums[i]<=nums[2i+2]\
堆排序的基本思路
堆排序的基本思想是:将待排序的数组构造成一个大顶堆,此时整个数组的最大值就是堆顶的根节点,将其与末尾节点的值进行交换,此时末尾就是最大值。然后将剩余n-1个元素重新构造成一个大顶堆,这样就得到了n个元素的次小值。如此反复执行,便可得到一个有序序列。
堆排序分两个阶段:
第一阶段:把数组构建成一个大顶堆。
第二阶段:交换堆顶元素和末尾元素,并从堆顶元素开始对剩下的元素构建大顶堆。
2.3.2 编码实现
public static void heapSort(int[] nums){
//把数组构建成大顶堆
for (int i=(nums.length/2-1);i>=0;i--){
adjustHump(nums,i,nums.length);
}
//交换堆顶元素和末尾元素,并从堆顶开始构建剩余元素为大顶堆
for (int j=nums.length-1;j>0;j--){
swap(nums,0,j);
adjustHump(nums,0,j);
}
}
//从父节点开始依次向下比较元素,构建大顶堆
public static void adjustHump(int[] nums,int i,int len){
int tmp = nums[i];
for (int k=2*i+1;k<len;k=2*k+1){
if (k+1<len && nums[k]<nums[k+1]){
k++;
}
if (nums[k]>tmp){
swap1(nums,i,k);
i = k;
} else {
break;
}
}
}
public static void swap1(int[] nums,int i,int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
再简单总结下堆排序的基本思路:
1)将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
2)将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
3)从堆顶元素开始重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
2.3.3 时间复杂度和空间复杂度
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以堆排序的时间复杂度 S(n) = O(NlogN)。
堆排序的过程中没有额外占用空间,是在原数组中进行的,所以空间复杂度 T(n) = O(1) 。
2.7 归并排序
2.3.1 解题思路
归并排序是使用分治策略(分治法是将大问题分成小问题,对小问题来求解,然后将分解的答案合并成大问题需要的答案)实现的排序算法,归并排序的核心思想是把数组 nums 一分为二,把分好的两个子数组分别进行排序,然后再合并两个排好的子数组得到合并后的有序数组,依次类推,直到把数组拆分的只剩一个元素,然后再依次合并。归并排序使用递归的思想,先依次拆分数组,然后再依次合并,跟快速排序某些程度上是相反的,快速排序是找目标值的位置然后拆分数组。
示例:
输入 nums = {9,4,5,2,1,7,4,6} 输出 nums = {1,2,4,4,5,6,7,9}
求解的过程如下图所示:
可以看到这种结构很像一个完全二叉树,树的深度是 logN,本文采用递归来实现归并,所以递归的深度为 logN。
如何合并两个有序数组?合并两个有序数组需要借助第三个数组来存储合并后的数组,假设数组 nums1 = {4,5,7,8} ,nums2 = {1,2,3,6},两个有序数组从左往后一次比较值的大小,把值小的放入第三个数组nums3中。这些需要数组下标,假设数组 nums1 下标从 0 开始 index1 = 0,数组 nums2 下标从 0 开始,index2 = 0。 执行步骤:
第1步:index1 = 0,index2 = 0,比较 4 和 1 ,1 小于 4 ,所以把 1 放入数组 nums3 中,此时数组情况:
nums1 = {4,5,7,8} ,nums2 = {1,2,3,6},nums3={1}
然后 index2++ = 1。
第2步:index1 = 0,index2 = 1,
nums1 = {4,5,7,8} ,nums2 = {1,2,3,6},nums3={1,2}
此时 index2++ = 2。
第3步:index1 = 0,index2 = 2,
nums1 = {4,5,7,8} ,nums2 = {1,2,3,6},nums3={1,2,3}
此时 index2++ = 3
第4步: index1 = 0,index2 = 3,
nums1 = {4,5,7,8} ,nums2 = {1,2,3,6},nums3={1,2,3,4}
此时 index1++ = 1
第5步: index1 = 1,index2 = 3,
nums1 = {4,5,7,8} ,nums2 = {1,2,3,6},nums3={1,2,3,4,5}
此时 index1++ = 2
第6步:index1 = 1,index2 = 3,
nums1 = {4,5,7,8} ,nums2 = {1,2,3,6},nums3={1,2,3,4,5,6}
此时nums2已经遍历完成。
第7步:依次将 nums1 中剩下的元素插入 nums3 中
nums3={1,2,3,4,5,6,7,8}
第8步:把nums3中的元素按顺序放入nums中,最终得到有序数组
nums = {1,2,3,4,5,6,7,8}
2.3.2 编码实现
int[] tmps = new int[nums.length];
public static void mergeSort(int[] nums,int left,int right){
if(left<right){
int mid = (left+right)/2;
mergeSort(nums,left,mid);
mergeSort(nums,mid+1,right);
sort(nums,left,mid,right);
}
}
public static void sort(int[] nums,int left,int mid,int right){
int leftIndex = left;
int rightIndex = mid+1;
int tmpIndex = left;
while(leftIndex<=mid && rightIndex<=right){
if(nums[leftIndex] <= nums[rightIndex]){
tmps[tmpIndex++] = nums[leftIndex++];
} else {
tmps[tmpIndex++] = nums[rightIndex++];
}
}
while(leftIndex<=mid){
tmps[tmpIndex++] = nums[leftIndex++];
}
while(rightIndex<=right){
tmps[tmpIndex++] = nums[rightIndex++];
}
for(int i=left;i<=right;i++){
nums[i] = tmps[i];
}
}
2.3.3 时间复杂度和空间复杂度
由归并排序的树形结构可以看到树的深度是 logN ,每一层最多需要N次比较,所以归并排序的时间复杂度 T(n)=O(NlogN)。归并排序的时候需要使用到额外的数组tmps,所以空间复杂度 S(n) = O(n)
2.8 计数排序
2.3.1 解题思路
计数排序是一种非比较排序,是典型的用空间换取时间的排序算法,时间复杂度只有 O(n)。
计数排序的思路非常简单:重新创建一个数据,把要排序的数组中的值作为新的数组的索引,新的数组的值是元素出现的次数,然后遍历新的数组,依次把大于0的元素的下标取出赋值到另一个数组,即为排序后的数组。计数排序的步骤:
1)找出原数组 nums 中的最大值 max 。
2)定义一个新的数组 int[] countNums = new int[max+1] 。
3)对新的数组 countNums 赋值,countNums 的索引是数组 nums 的元素值,countNums 的值是索引值在nums中出现的次数。
4)定义nums的索引值 index=0 , 遍历数组 countNums ,依次把值大于0的索引值放入数组nums中。遍历的时候countNums的值每次减 1 ,直到为0为止。
计数排序的过程如下图所示:
2.3.2 编码实现
public static void countSort(int[] nums){
//找出数组中的最大值
int max = -1;
for(int i=0;i<nums.length;i++){
if(max<nums[i]){
max = nums[i];
}
}
//定义新数组,并赋值
int[] countNums = new int[max+1];
for(int i=0;i<nums.length;i++){
countNums[nums[i]]++;
}
//把元素赋值到目标数组中,完成排序
int index = 0; //nums数组的索引
for(int i=0;i<countNums.length;i++){
while(countNums[i]-->0){
nums[index++] = i;
}
}
}
2.3.3 时间复杂度和空间复杂度
技术排序是线性时间,所以时间复杂度 S(n) = O(n)。计数排序额外使用了元素最大值的空间,所以空间复杂度 S(n) = O(n)。
技术排序需要满足两个条件:第一,数组的值相对比较集中;第二,排序的值必须是整数。
2.9 桶排序
2.3.1 解题思路
桶排序是最简单的排序算法之一,桶排序和计数排序、基数排序有很多相似和渊源之处。桶排序重要的是它的思路,而不是具体的实现。不同待排序元素,桶排序的具体实现会有差异。
桶排序的工作原理是把待排序的元素放入对应的桶里。每个桶再进行单独的排序(可以是快速排序、可以是递归排序、也可以用递归的方式继续使用桶排序进行排序)。桶排序需要经过四步:
1)按照元素特性设计好桶;
2)把待排序元素放入对应的桶中;
3)对每个桶内的元素进行排序;
4)合并桶中的元素得到有序序列。
既然是排序,最终的结果要么是从大到小,要么是从小到大,桶排序首先把桶的位置排好,然后一次把元素分别放入各个桶中。确定桶的数量有很多种,可以根据待排序元素的特性来定义桶的数量和元素放入桶的规则。通常根据待排序元素整除的方法将其均匀放入桶中。
示例:
输入 nums = {7,10,21,6,13,39,58,22,40,52,32,50,46,29}
输出 nums = {6,7,10,13,21,22,29,32,39,40,46,50,52}
观察数组中元素的特性,可以设计出放入桶编号的规则为:元素值/10。这样每个元素都可以通过整除的方法放至对应的桶中。而左侧所有的桶内的元素都要比右侧桶内元素的值小。放置的过程如下图所示:
在刚刚放入桶中的时候,各个桶的大小相对可以确定,左侧比右侧要小,但是桶内还是无序的,对各个桶内分别进行排序,桶内元素排序的排序算法可以自行选择:
再依次按照桶的顺序以及桶内序列得到一个最终的有序序列。
2.3.2 编码实现
编码实现一:
可以使用计数排序的思想,桶里放的是元素出现的次数。桶的编号是元素的值。编码如下所示:
public static void bucketSort(int[] nums){
//找到最大值和最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i=0;i<nums.length;i++){
max = Math.max(max,nums[i]);
min = Math.min(min,nums[i]);
}
int k = max - min+1;//计算桶的个数
int[] tmp = new int[k];//定义出K个桶
//计算出元素出现的次数,计入桶中
for (int i=0;i<nums.length;i++){
tmp[nums[i]-min]++;
}
//将待排序的数组放入自己的位置
int x = 0;
for (int i=0;i<tmp.length;i++){
while (tmp[i]-->0){
nums[x++] = i+min;
}
}
}
编码实现二:
使用链表存储桶内的元素,链表中的元素依次排序,再合并链表最终得到有序数组。
public static void bucketSort1(int[] nums){
//找到最大值和最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i=0;i<nums.length;i++){
max = Math.max(max,nums[i]);
min = Math.min(min,nums[i]);
}
int k = (max-min)/nums.length+1;
ArrayList<ArrayList> list = new ArrayList<ArrayList>(k);
for (int i=0;i<k;i++){
list.add(new ArrayList<Integer>());
}
for (int i=0;i<nums.length;i++){
list.get((nums[i]-min)/nums.length).add(nums[i]);
}
for (int i=0;i<list.size();i++){
Collections.sort(list.get(i));
}
System.out.println(list.toString());
}
2.3.3 时间复杂度和空间复杂度
时间复杂度
假设有n
个待排序数字。分到m
个桶中,如果分配均匀这样平均每个桶有n/m
个元素。桶排序的算法时间复杂度有两部分组成:
- 遍历处理每个元素,O(n)级别的普通遍历
- 每个桶内再次排序的时间复杂度总和
对于第一个部分,大家都应该理解最后排好序的取值遍历一趟的O(n),而第二部分咱们可以进行这样的分析:
- 如果桶内元素分配较为均匀假设每个桶内部使用的排序算法为快速排序,那么每个桶内的时间复杂度为
(n/m) log(n/m)
。有m个桶,那么时间复杂度为m * (n/m)log(n/m)
=n (log n-log m)
.
所以最终桶排序的时间复杂度为:O(n)+O(n*(log n- log m))
=O(n+n*(log n -log m))
其中m为桶的个数。我们有时也会写成O(n+c),其中c=n*(log n -log m);
在这里如果到达极限情况n=m
时。就能确保避免桶内排序,将数值放到桶中不需要再排序达到O(n)的排序效果,当然这种情况属于计数排序,后面再详解计数排序记得再回顾。
空间复杂度
桶排序是典型的使用空间换取时间的排序算法。是计数排序的扩展。桶排序要定义出多个桶,能够容纳所有的待排序元素,如果桶内使用快速排序等空间复杂度为O(1)的排序算法,那么桶排序的空间复杂度取决于桶的容量综合,S(n) = O(n+1)。如果桶内使用的是其他需要额外空间的排序算法,那么桶排序的空间复杂度是两个空间复杂度的综合,S(n) = O(n+k)。
2.10 基数排序
2.3.1 解题思路
基数排序的核心思想是把要比较的元素拆分,按照个位、十位、百位、千位的来拆分,然后在相同位上的数值放入从0-9的10个桶中,这样相同位上的数值就有序了,然后依次类推进行比较,最终得到排好序的元素列表。
基数排序有两种方式,一种是最高位优先(Most Significant Digit first)法,简称MSD法。一种是最低位优先(Least Significant Digit first)法,简称LSD法。
示例:
输入:nums = {80, 55, 70, 50, 66, 70, 71, 91, 90, 98, 99, 82, 85, 101, 78, 77, 76, 68, 50, 54}
输出:nums = {50, 50, 54, 55, 66, 68, 70, 70, 71, 76, 77, 78, 80, 82, 85, 90, 91, 98, 99, 101}
第一步:遍历个位的数值,放入0-9的桶中
桶 0 :80,70,50,70,90,50
桶 1 :71,91,101
桶 2 :82
桶 3 :
桶 4 :54
桶 5 :55,85
桶 6 :66,76
桶 7 :77
桶 8 :98,78,68
桶 9 :99
然后合并桶中的元素得到数组:
nums = {80,70,50,70,90,50,71,91,101,82,54,55,85,66,76,77,98,78,68,99}
第二步:遍历十位的数值,放入0-9的桶中
桶 0 :101
桶 1 :
桶 2 :
桶 3 :
桶 4 :
桶 5 :50,50,54,55
桶 6 :66,68
桶 7 :70,70,71,76,77,78
桶 8 :80,82,85
桶 9 :90,91,98,99
然后合并桶中的元素得到数组:
nums = {101,50,50,54,55,66,68,70,70,71,76,77,78,80,82,85,90,91,98,99}
第三步:遍历百位的数值,放入0-9的桶中
桶 0 :50,50,54,55,66,68,70,70,71,76,77,78,80,82,85,90,91,98,99
桶 1 :101
桶 2 :
桶 3 :
桶 4 :
桶 5 :
桶 6 :
桶 7 :
桶 8 :
桶 9 :
然后合并桶中的元素得到数组:
nums = {50,50,54,55,66,68,70,70,71,76,77,78,80,82,85,90,91,98,99,101}
可以看到数组已经是有序了。
2.3.2 编码实现
public static void binSort(int[] nums,int order){
int[][] tmpNums = new int[10][nums.length];//二维数组,数组的第一维标识可能的余数0-9
int[] seat = new int[10];//记录元素在二维数组中的位置
int m = 1;//从哪一位开始循环排序
int t = 1;//十位除以10,百位除以100
int mark = 0;//元素下标
int k = 0;//目标数组下标;
while (m<=order) {
//把待排序元素放入二维数组中
for (int i = 0; i < nums.length; i++) {
mark = (nums[i]/t)%10;
tmpNums[mark][seat[mark]++] = nums[i];
}
//把二维数组中的元素放入目标数组中
for (int i = 0; i < 10; i++) {
if(seat[i] != 0) {
for (int j = 0; j < seat[i]; j++) {
nums[k++] = tmpNums[i][j];//二维数组的值放入目标数组中
}
}
seat[i] = 0;//清空一维数组的值,以便下次使用
}
m++;
t = 10*t;
k = 0;
}
编码的实现对应解题思路的步骤可以分析一下:使用二维数组来存储排序中的元素,二维数组的第一维表示0-9的桶。执行过程如下所示:
第一步:
80 70 50 70 90 50 0 0 0 0 0 0 0 0 0 0 0 0 0 0
71 91 101 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
82 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
54 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
55 85 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
66 76 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
77 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
98 78 68 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
99 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
第二步:
101 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
50 50 54 55 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
66 68 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
70 70 71 76 77 78 0 0 0 0 0 0 0 0 0 0 0 0 0 0
80 82 85 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
90 91 98 99 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
第三步:
50 50 54 55 66 68 70 70 71 76 77 78 80 82 85 90 91 98 99 0
101 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
2.3.3 时间复杂度和空间复杂度
设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度 T(n) = O(d(n+radix)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集。 空间效率:需要2*radix个指向队列的辅助空间,以及用于静态链表的n个指针。
3 排序算法性能对比
排序算法 | 平均时间复杂度 | 最坏情况 | 最好情况 | 空间复杂度 | 稳定性 |
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n^(3/2)) | O(n^2) | O(n^(3/2)) | O(1) | 不稳定 |
选择排序 | O(n^2) | O(n^2) | O(n) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
桶排序 | O(n+k) | O(n^2) | O(n+k) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |