排序算法
〇、总览
(一)术语说明
稳定 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定 :如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
内排序 :所有排序操作都在内存中完成;
外排序 :由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
时间复杂度 : 一个算法执行所耗费的时间。
空间复杂度 :运行完一个程序所需内存的大小。是对一个算法在运行过程中临时占用存储空间大小的量度 ,也就是额外占取的空间的大小。
(二)概览
n: 数据规模。 k: “桶”的个数。 In-place: 占用常数内存,不占用额外内存。 Out-place: 占用额外内存
(三)算法分类
(四)比较和非比较的区别
常见的快速排序、归并排序、堆排序、冒泡排序 等属于比较排序 。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置 。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
计数排序、基数排序、桶排序则属于非比较排序 。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置 。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
一、冒泡排序(Bubble Sort)
左边大于右边交换一趟排下来最大的在右边
(一)算法描述
1.比较相邻的元素。如果第一个比第二个大,就交换它们两个;(目标是升序)
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
3.针对所有的元素重复以上的步骤,除了最后一个;
4.重复步骤1~3,直到排序完成。
(二)算法分析
1.分析
若没有使用提前停止或者每次扫描都有元素交换(也就是最坏情况),扫描次数为:
时间复杂度:最坏情况:O(N2)(每次有元素交换,待排序序列是反序)(需n * (n-1) / 2次比较, 以及n * (n-1) / 2次交换)
最好情况:O(N)(本来就是顺序)(需n-1次比较)
冒泡排序总的平均时间复杂度为O(N2)。
空间复杂度:O(1)
2.适用场景
冒泡排序适用于数据量很小的排序场景,因为冒泡的实现方式较为简单。
3.稳定性
在冒泡排序中,遇到相等的值,是不进行交换的,只有遇到不相等的值才进行交换,所以是稳定的排序方式。
(三)代码实现
void bubbleSort(int a[], int n) { for(int i = n - 1; i > 0; i--) for(int j = 0; j < i; j++) if(a[j] > a[j+1]) //若目标是升序 swap(a[j], a[j+1]); }
(四)优化
上面的两种优化都是单向遍历比较的,然而在很多时候,遍历的过程可以从两端进行,从而提升效率。因此在冒泡排序中,其实也可以进行双向循环,正向循环把最大元素移动到数组末尾,逆向循环把最小元素移动到数组首部。该种排序方式也叫双向冒泡排序,也叫鸡尾酒排序。
static void bubbleSort(int[] array) { int arrayLength = array.length; int preIndex = 0; int backIndex = arrayLength - 1; while(preIndex < backIndex) { preSort(array, arrayLength, preIndex); preIndex++; if (preIndex >= backIndex) { break; } backSort(array, backIndex); backIndex--; } } // 从前向后排序 static void preSort(int[] array, int length, int preIndex) { for (int i = preIndex + 1; i < length; i++) { if (array[preIndex] > array[i]) { swap(array, preIndex, i); } } } // 从后向前排序 static void backSort(int[] array, int backIndex) { for (int i = backIndex - 1; i >= 0; i--) { if (array[i] > array[backIndex]) { swap(array, i, backIndex); } } } static void swap (int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
二、选择排序(Selection Sort)
选择排序 是表现最稳定的排序算法之一 ,因为无论什么数据进去都是O(n2)的时间复杂度
(一)算法描述
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,
然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。
以此类推,直到所有元素均排序完毕。
(二)算法分析
1.分析
时间复杂度:最坏情况:O(N2)
最好情况:O(N2)
空间复杂度:O(1)
2.适用场景
待排序序列中,元素个数较少时。
3.稳定性
选择排序是不稳定排序。假设有一个数组`array={1,2,51,52,3}
这里省略具体排序细节
1.第一轮排序,用1和右侧所有元素比较,1最小,不换位array={1,2,51,52,3}
2.第二轮排序,用2和2后面的所有元素比较,2最小,不换位array={1,2,51,52,3}
3.第三轮排序,用第三个5和其后面元素比较,3最小,换位array={1,2,3,52,51}
4.注意,本来在下标2位置的51和元素3(下标4)换位后,被移动到了数组末尾。而这个51和下标3位置的52大小相等。
5.第四轮,用下标3位置的52和最后一个元素51比较,相等,不换位。得出数组array={1,2,3,52,51}
(三)代码实现
for (int i = 0; i < arr.length; i++) { //对数组中下标为i的位置进行选择排序 int minpos = i; for (int j = i; j < arr.length - 1; j++) { //寻找数组i+1位置之后,最小的值 minpos = arr[j] < arr[minpos] ? j : minpos; //将最小值所在数组下表放入minpos中 } swap(arr, i, minpos); //将此时i的值与minpos交换 }
(四)优化
实际上,我们可以一趟选出两个值,一个最大值一个最小值,然后将其放在序列开头和末尾,这样可以使选择排序的效率快一倍。
void SelectSort(int* arr, int n) { //保存参与单趟排序的第一个数和最后一个数的下标 int begin = 0, end = n - 1; while (begin < end) { //保存最大值的下标 int maxi = begin; //保存最小值的下标 int mini = begin; //找出最大值和最小值的下标 for (int i = begin; i <= end; ++i) { if (arr[i] < arr[mini]) { mini = i; } if (arr[i] > arr[maxi]) { maxi = i; } } //最小值放在序列开头 swap(&arr[mini], &arr[begin]); //防止最大的数在begin位置被换走 if (begin == maxi) { maxi = mini; } //最大值放在序列结尾 swap(&arr[maxi], &arr[end]); ++begin; --end; } }
三、插入排序(Insertion Sort)
(一)算法描述
1.例子
例如要将数组arr=[4,2,8,0,5,1]排序,可以将4看做是一个有序序列,将[2,8,0,5,1]看做一个无序序列。
无序序列中2比4小,于是将2插入到4的左边,此时有序序列变成了[2,4],无序序列变成了[8,0,5,1]。无序序列中8比4大,于是将8插入到4的右边,有序序列变成了[2,4,8],无序序列变成了[0,5,1]。
2.描述
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
(二)算法分析
1.分析
时间复杂度:最坏情况下为O(N2)(此时待排序列为逆序,或者说接近逆序)
若假设平均情况为插入时,找已经排好序的一半即可,那么也是O(N2)
最好情况下为O(N)(此时待排序列为顺序,或者说接近顺序)
The running time of insertion sort is a constant times the number of inversions.
空间复杂度:O(1)
2.适用场景
待排序序列的元素个数不多(<=50),且元素基本有序。
3.稳定性
在使用插入排序时,元素从无序部分移动到有序部分时,必须是不相等(大于或小于)时才会移动,相等时不处理,所以直接插入排序是稳定的。
4.插入排序和冒泡排序的优劣性比较
逆序对定义为数组中 i < j ,但 A [ i ] > A [ j ] 的序偶对
数组中逆序对中的数量即执行插入排序需要进行的交换次数。情况总是这样,因为交换两个不按原序排序的元素恰好消除一个逆序,而一个排过序的数组没有逆序。
一般来说,插入排序较冒泡排序性能更胜一筹,为什么那? 因为冒泡排序的交换次数和插入排序的移动次数都是由逆序对的数量决定的,但是冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个。所以,插入排序的实际运行时间要优于冒泡排序。
(三)代码实现
//每次将从0-i位置的元素进行排序 for (int i = 1; i < arr.length; i++) { // 0 ~ i 做到有序 //int j = i - 1; j >= 0表示左边位置的边界条件 //arr[j] > arr[j + 1]表示最右边的数与相邻数的比较 //j--表示将这个过程向左推进 //swap(arr, j, j + 1)表示进行两个相邻数比较时,左边的数大于右边数时,才交换否则不交换 for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) { swap(arr, j, j + 1); } }
(四)算法优化
折半插入排序
该类优化有二分的思想,是在将待排序的元素与有序部分的元素比较时,不再挨个比较,而是用二分折中的方式进行比较,加快比较效率。示例代码如下:
int j, low, mid, high, temp; for (int i = 1; i < n; i++) { low = 0; high = i - 1; temp = arr[i]; /*找到合适的插入位置high+1,如果中间位置元素 *比要插入元素大,则查找区域向低半区移动,否 *则向高半区移动 */ while (low <= high) { mid = (low + high) / 2; if (arr[mid] > temp) { high = mid - 1; } else { low = mid + 1; } } /*high+1后的元素后移*/ for (j = i - 1; j >= high + 1; j--) { arr[j + 1] = arr[j]; } /*将元素插入到指定位置*/ arr[j + 1] = temp; }
四、希尔排序(Shell Sort)
希尔排序是希尔(Donald Shell) 于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
我们发现,当被排序的对象越接近有序时,插入排序的效率越高,那我们是否有办法将数组变成接近有序后再用插入排序,此时希尔大佬就发现了这个排序算法,并命名为希尔排序。
其算法的基本思想是:先将待排记录序列分割成为若干子序列分别进行插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序。
(一)算法描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 步骤1:选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;(增量序列的最后一个增量值必须是1)
- 步骤2:按增量序列个数k,对序列进行k 趟排序;
- 步骤3:将整个待排序列分割成若干个子序列(由相隔增量个元素组成,增量也就是ti),分别进行直接插入排序,然后依次缩小增量再进行排序,待整个序列中的元素基本有序时,再对全体元素进行一次直接插入排序。仅增量因子为1 时(即相隔一个元素了,tk=1),整个序列作为一个表来处理,表长度即为整个序列的长度。
(二)算法分析
1.分析
时间复杂度取决于所用序列:
空间复杂度:O(1)
2.适用场景
待排序序列元素较少时。
3.增量序列
选择不同的增量,拥有不同的时间复杂度。
最初希尔提出的增量是 gap = n / 2,每一次排序完让增量减少一半gap = gap / 2,直到gap = 1时排序变成了直接插入排序。
直到后来Knuth提出的gap = [gap / 3] + 1,每次排序让增量成为原来的三分之一,加一是防止gap <= 3时gap = gap / 3 = 0的发生,导致希尔增量最后不为1,无法完成插入排序。
到目前为止业内对于各种方法依然是看法不一,都没有比出个上下出来。
下面使用Knuth提出的除三法获得希尔增量来演示
4.稳定性
希尔排序是直接插入排序的优化版,在排序过程中,会根据间隔将一个序列划分为不同的逻辑分组,在不同的逻辑分组中,有可能将相同元素的相对位置改变。如[2,2,4,1],按间隔为2,降序排序,前两个元素的相对位置就会改变。因此,希尔排序是不稳定的排序方式。
(三)代码实现
/*初始化划分增量*/ int increment = len; int temp; /*每次减小增量,直到increment = 1*/ while (increment > 1) { /*增量的取法之一:除三向下取整+1*/ increment = increment / 3 + 1; /*对每个按增量划分后的逻辑分组,进行直接插入排序*/ for (int i = increment; i < len; ++i) { if (arr[i - increment] > arr[i]) { temp = arr[i]; int j = i - increment; /*移动元素并寻找位置*/ while (j >= 0 && arr[j] > temp) { arr[j + increment] = arr[j]; j -= increment; } /*插入元素*/ arr[j + increment] = temp; } } }
(四)算法优化
因为是基于插入排序,所以希尔排序的优化可以借鉴简单插入排序的优化方法处理,例如:希尔+二分插入排序
/*初始化划分增量*/ int increment = len; int j, temp, low, mid, high; /*每次减小增量,直到increment = 1*/ while (increment > 1) { /*增量的取法之一:除三向下取整+1*/ increment = increment / 3 + 1; /*对每个按增量划分后的逻辑分组,进行直接插入排序*/ for (int i = increment; i < len; ++i) { low = 0; high = i - 1; temp = arr[i]; while (low <= high) { mid = (low + high) / 2; if (arr[mid] > temp) { high = mid - 1; } else { low = mid + 1; } } j = i - increment; /*移动元素并寻找位置*/ while (j >= high + 1) { arr[j + increment] = arr[j]; j -= increment; } /*插入元素*/ arr[j + increment] = temp; } }
五、归并排序(Merge Sort)
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
归并排序 是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
(一)算法描述
- 步骤1:把长度为n的输入序列分成两个长度为n/2的子序列;
- 步骤2:对这两个子序列分别采用归并排序;
- 步骤3:将两个排序好的子序列合并成一个最终的排序序列。
步骤3,合并相邻有序子序列方法:
(二)算法分析
1.时间复杂度
归并排序算法每次将序列折半分组,共需要logn轮(完全二叉树的深度为|log2n|),因此归并排序算法的时间复杂度是O(nlogn)。
而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。
其中常量c代表求解规模为 1的问题所需的时间以及在分解步骤与合并步骤处理每个数组元素所需要的时间
2.空间复杂度
归并排序算法排序过程中需要额外的一个序列去存储排序后的结果,所占空间是n,因此空间复杂度为O(n)
3.稳定性
归并排序算法在排序过程中,相同元素的前后顺序并没有改变,所以归并排序是一种稳定排序算法
(三)代码实现
void merge(int arr[], int l1, int mid, int r2, int temp[]) { int i = l1, j = mid + 1; int p = 0; while (i <= mid && j <= r2) { if (arr[i] > arr[j]) { temp[p] = arr[j]; p++; j++; } else { temp[p] = arr[i]; p++; i++; } } while (i <= mid) { temp[p] = arr[i]; p++; i++; } while (j <= r2) { temp[p] = arr[j]; p++; j++; } //覆盖数组 int n = r2 - l1 + 1; for (int i = 0; i < n; i++) { arr[l1 + i] = temp[i]; } } void mergeSort(int arr[], int left, int right,int temp[]) { if (left < right) { int mid = (left + right) / 2; mergeSort(arr, left, mid,temp); //分 mergeSort(arr, mid + 1, right,temp); merge(arr, left, mid,right,temp); //合 } }
(四)算法优化
- 对小规模子数组使用插入排序,小规模范围内插入排序性能更优
- 测试子数组是否已经有序,如果有序则不需要合并
void mergeSort(vector<int>& nums, int left, int right){ if(left >= right) return; int mid = left+(right-left)/2; mergeSort(nums, left, mid); mergeSort(nums, mid+1, right); //测试子数组是否已经有序 if(nums[mid] > nums[mid+1]){//就是0,2,4<5,9,67 merge(nums, left, mid, right); } }
- 不将元素复制到辅助数组
六、快速排序(Quick Sort)
在一个无序数组中取一个数key,每一趟排序的最终目的是:让key的左边的所有数小于key,key的右边都大于key(假设排升序)。
(一)算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
步骤1:从数列中挑出一个元素,称为 “基准”(pivot );
步骤2:重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
步骤3:递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
在选择基准值的时候,越靠近中间,性能越好;越靠近两边,性能越差。
(二)算法分析
1.时间复杂度
最佳情况:T(n) = O(nlogn) (每次数据元素都能平均的分成两个部分。)
最差情况:T(n) = O(n2)
在最坏的情况下,这个数仅有右子树或左子树,比较次数为 (n-1)+(n-2) + (n-3) + … +1=n*(n-1)/2 ,因此时间复杂度为O(n2),在待排序数据元素已经有序的情况下快速排序时间复杂度最高
平均情况:T(n) = O(nlogn)
2.空间复杂度
(1)原地排序
最优的情况下空间复杂度为:O(logn) ;每一次都平分数组的情况
最差的情况下空间复杂度为:O(n) ;退化为冒泡排序的情况
首先就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据;
(2)非原地排序
对于非原地排序,每次递归都要声明一个总数为n的额外空间,所以空间复杂度变为原地排序的n倍
最好情况下O(nlogn) ,最差情况下O(n2)。
3.稳定性
以全1数组为例,当游标所指向的元素同枢纽元相等时选择了继续前进,而不是停留在原地,这就导致游标肆无忌惮的一路向前,根本没有顾及到左右两个数组是否均衡,因而触犯了“大忌”,左右两边的数组明显失衡。
避免踩坑: 当游标所指向的元素同枢纽元相等时,待在原地不动,左右游标指向的元素进行交换,再各自向前移动。
快速排序出于性能上的考虑,牺牲了算法的稳定性。虽然可以改变交换规则,使得算法保持稳定性,但却牺牲了性能,对于一个排序算法而言,这是得不偿失的,除非是上下文有稳定性方面的需求,否则,不建议改变交换规则。
4.适用场景
快速排序的适用场景是:待排序序列元素较多,并且元素较无序。
(三)代码实现
(四)算法优化
1.三数取中
该方法指的是选取基准值时,不再取固定位置(如第一个元素、最后一个元素)的值,因为这种固定取值的方式在面对随机输入的数组时,效率是非常高的。但是一旦输入数据是有序的,使用固定位置取值,效率就会非常低。因此此时引入了三数取中,即在数组中随机选出三个元素,然后取三者的中间值做为基准值。
2.插入排序
当待排序序列的长度分割到一定大小(如 < 10)后,使用插入排序。
3.相等元素聚集
在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割。
七、堆排序(Heap Sort)
利用堆这种数据结构将一组数据升序或降序,升序建大堆,降序建小堆
(一)算法描述
1.升序
先建立大根堆,然后交换堆顶元素和堆底元素,再将堆的长度减1,直至堆中只有一个元素。
2.降序
先建立小根堆,然后交换堆顶元素和堆底元素,再将堆的长度减1,直至堆中只有一个元素。
(二)算法分析
1.升序
时间复杂度:O(nlog2n)
空间复杂度:O(1)
2.降序
时间复杂度:O(nlog2n)
空间复杂度:O(1)
(三)代码实现
1.升序
//堆排序(升序) void HeapSortUp(int* arr, int n) { //向下调整建大堆 for (int i = (n - 1) / 2; i >= 0; i--) { AdjustDown(arr, n, i); } int end = n - 1; while (end > 0) { Swap(&arr[0], &arr[end]); AdjustDown(arr, end, 0); end--; } }
2.降序
//堆排序(降序) void HeapSortDown(int* arr,int n) { //向下调整建小堆 for (int i = (n - 1) / 2; i >= 0; i--) { AdjustDown(arr, n, i); } int end = n - 1; while (end > 0) { Swap(&arr[0], &arr[end]); AdjustDown(arr, end, 0); end--; } }
(四)算法优化
堆排序的优化算法主要是对空间复杂度进行优化。由于我们之前建堆都要开辟新的数组,因此我们是否可以在原数组上直接建堆,无需再开辟新的空间建堆呢?答案当然是可以的。
八、计数排序(Counting Sort)
计数排序不是一个比较排序算法,是一种牺牲空间来换取时间的排序算法
(一)算法描述
1、找出待排序的数组中最大和最小的元素
2、建立一个max的数组空间,每个元素初始值为0
3、遍历要排序的无序数组
比如说,第一个数是2,那么数组下标为2的元素加1
第二个数是3,那么数组下标为3的元素加1
以此类推
(4、对所有的计数累加(从中的第一个元素开始,每一项和前一项相加);)
5、到最后,我们就得到了一个数组,数组中每一个值,代表了下标值在排序数组内出现的次数。
(6、直接遍历我们得到的数组,输出下标值,元素的值为多少,就输出几次。)
排序完成
(二)算法分析
n表示的是数组的个数,k表示的max-min+1的大小
1.时间复杂度
时间复杂度是O(n+k)
2.空间复杂度
O(k)
3.稳定性
计数排序 稳定
统计相同数值的个数,按顺序映射到原数组
4.适用场景
计数排序适用于:
- 序列中最大值和最小值之间的差值不能过大,这主要是防止建立数组时造成内存的浪费。
- 序列中存在的元素是整数,因为我们使用的是该元素作为键存储在额外的数组空间中,如果不是整数,不能作为键。
(三)代码实现
/** 定义counting_sort函数用递归方法实现计数排序 Input: A[]:int[],是需要排序的数组. 指针传递,函数内部对A的修改能导致外部A的变化 B[]:int[],是存储排序后的数组. 指针传递,函数内部对B的修改能导致外部B的变化 k: int,是待排序数组的最大值. n: int,是待排序数组的长度. Output: B: int*, 排序后的数组. **/ void counting_sort(int A[], int B[], int k, int n) { int *C = new int[k + 1]; // 因为数组下标是从0开始的, 所以要到k的话, 需要k+1 for (int i = 0; i < k + 1; i++) C[i] = 0; for (int j = 0; j < n; j++) C[A[j]] = C[A[j]] + 1; for (i = 1; i < k + 1; i++) C[i] = C[i] + C[i - 1]; for (j = n - 1; j >= 0; j--) { B[C[A[j]] - 1] = A[j]; // 这里-1是因为数组B的序号是从0开始的, 而伪代码的序号是从0开始的 C[A[j]] = C[A[j]] - 1; } delete[] C; }
(四)算法优化
我们可以通过使用使用数列的最大值-最小值+1
作为统计数组的长度即可,同时使用最小值作为一个偏移量,用于计算整数在统计数组中的下标。
九、桶排序(Bucket Sort)
桶排序的思想近乎彻底的分治思想。
(一)算法描述
桶排序的思想是将一组数据分到几个有序的桶里,每个桶里的数据再单独进行快速排序。每个桶内都排序完成后,再加上本身桶之间已经是有序的,那么按照桶的顺序依次取出每个桶内的数据,最终组成的序列就是有序的。
- 1.设置一个定量的数组当作空桶;
- 2.遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 3.对每个不是空的桶进行排序;
- 4.从不是空的桶里把排好序的数据拼接起来。
(二)算法分析
1.时间复杂度
我们假设有n
个待排序数字。分到m
个桶中,如果分配均匀这样平均每个桶有n/m
个元素。首先在这里我郑重说明一下桶排序的算法时间复杂度有两部分组成:
- 1.遍历处理每个元素,O(n)级别的普通遍历
- 2.每个桶内再次排序的时间复杂度总和
对于第一个部分,我想大家都应该理解最后排好序的取值遍历一趟的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)的排序效果,当然这种情况属于计数排序,后面再详解计数排序记得再回顾。
2.空间复杂度
由于需要分配k个桶,且k个桶一共存储了N个元素,故其空间复杂度为 O(N + k)
3.稳定性
由于桶排序是如果基于快排实现,就不是稳定的排序算法。
如果是基于归并排序实现的,则可以完成稳定的排序。
4.适用场景
桶排序并且像常规排序那样没有限制,桶排序有相当的限制。因为桶的个数和大小都是我们人为设置的。而每个桶又要避免空桶的情况。所以我们在使用桶排序的时候即需要对待排序数列要求偏均匀,又要要求桶的设计兼顾效率和空间。
为了使桶排序更加高效,我们需要做到这两点:
1、在额外空间充足的情况下,尽量增大桶的数量;
2、使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中;
桶排序适合用在外部排序中,就是数据存储在外部磁盘上,由于服务器内存有限无法一次性加载到内存中。
比如现在有10G订单数据存在外部磁盘的一个文件中,我们想将这10G的订单数据按照从小到大进行排序,但是由于服务器内存有限只有几百M,无法一次性加载内存,这时候就可以利用桶排序进行解决了。
(三)代码实现
int* sort_array(int *arr, int n) { int i; int maxValue = arr[0]; for (i = 1; i < n; i++) if (arr[i] > maxValue) // 输入数据的最大值 maxValue = arr[i]; // 设置10个桶,依次0,1,,,9 const int bucketCnt = 10; vector<int> buckets[bucketCnt]; // 桶的大小bucketSize根据数组最大值确定:比如最大值99, 桶大小10 // 最大值999,桶大小100 // 根据最高位数字映射到相应的桶,映射函数为 arr[i]/bucketSize int bucketSize = 1; while (maxValue) { //求最大尺寸 maxValue /= 10; bucketSize *= 10; } bucketSize /= 10; //桶的个数 // 入桶 for (int i=0; i<n; i++) { int idx = arr[i]/bucketSize; //放入对应的桶 buckets[idx].push_back(arr[i]); // 对该桶使用插入排序(因为数据过少,插入排序即可),维持该桶的有序性 for (int j=int(buckets[idx].size())-1; j>0; j--) { if (buckets[idx][j]<buckets[idx][j-1]) { swap(buckets[idx][j], buckets[idx][j-1]); } } } // 顺序访问桶,得到有序数组 for (int i=0, k=0; i<bucketCnt; i++) { for (int j=0; j<int(buckets[i].size()); j++) { arr[k++] = buckets[i][j]; } } return arr; }
(四)算法优化
选择合适的桶内排序算法:桶内排序算法对于每个桶内的元素进行排序。可以根据元素的特性来选择合适的内部排序算法。
十、基数排序(Radix Sort)
(一)算法描述
(二)算法分析
(三)代码实现
(四)算法优化
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了