前言
名词解释:n:数据规模 k:桶的个数 In-place:占据常数内存 out-place:占额外内存
比较排序:常见的快速排序、归并排序、堆排序、冒泡排序 等属于比较排序 。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置 。
优势:适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
非比较排序:非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置 。
特点:非比较排序时间复杂度底(O(n)),但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
1.冒泡排序
算法思想:从头开始,每次比较两元素,若大者在前,则交换两元素,直至数组末尾,此时最大元素为数组最后的元素。除去最右的元素,我们对剩余的元素做同样的工作,如此重复下去,直到排序完成。
稳定算法
//冒泡排序,基础版,时间复杂度O(n^2)
int bubblesort(vector<int>& array){
int n=array.size();
if(n<2){return array;}
for(int i=0;i<n;i++){
for(int j=0;j<n-1-i;j++){
if(array[j+1]<array[j]){
int temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
}
}
}
return array;
}
//优化版,加一个标志位,判断当前轮是否发生过交换事件标志位,若这一轮都没有未发生交换,则表明列表已有序,直接退出。
int bubblesort(vector<int>& array){
int n=array.size();
if(n<2){return array;}
for(int i=0;i<n;i++){
bool isExchange=false;
for(int j=0;j<n-1-i;j++){
if(array[j+1]<array[j]){
int temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
isExchange=true;
}
}
if(!isExchange){
break;
}
}
return array;
}
2.选择排序
算法思想:
搜索整个列表,找出最小项,若此项不为第1项,则与第1项交换位置;
重复上述步骤,每次搜索未被排序的剩余列表,并将最小元素与已排序段的后一位交换,直至列表所有元素均被排序
//选择排序 时间复杂度O(n^2)
int selectSort(vector<int> arr){
int n =arr.size();
for(int i=0;i<n-1;i++){
int min=i;
for(int j=i+1;j<n;j++){
if(arr[min]>arr[j]){
min=j;
}
}
int temp=a[i];
arr[i]=arr[min];
arr[min]=temp;
}
return arr;
}
3.插入排序
算法思想:将无序元素插到有序元素中去
步骤1: 从第一个元素开始,该元素可以认为已经被排序;
步骤2: 取出下一个元素,在已经排序的元素序列中从后向前扫描;
步骤3: 如果该元素(已排序)大于新元素,将该元素移到下一位置(腾位置);
步骤4: 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
步骤5: 将新元素插入到该位置后;
步骤6: 重复步骤2~5。
//时间复杂度O(n^2),不稳定,
//插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序)
int insertSort(vector<int> arr){
int n=arr.size();
for(int i=1;i<n;i++){
j=i-1;
temp=arr[i];
while(j>=0&&arr[j]>temp){
arr[j+1]=arr[j];
j--;
}
arr[j+1]=temp;
}
return arr;
}
4.希尔排序
希尔排序是希尔(Donald Shell) 于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本。 希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
步骤
选择一个增量序列,初始增量gap=length/2,后续元素依次为前一元素除2,直至gap=1;
每轮以gap为步长,在列表上进行采样,将列表分为gap个小组,在每个小组内进行选择排序;
重复第二步,直至gap=1;
O
//希尔排序,O(nlogn),不稳定
//核心思想还是使用插入排序算法
//通过分组,让数据在小规模内有序,减小递归增量使得整体有序
int shellSort(vector<int> arr){
int n=arr.size();
int gap=n/2;
while(gap>0){
for(int i=gap;i<n;i++){
temp=arr[i]
int j=i-gap;
while(j>=0&&arr[j]>temp){
arr[j+gap]=arr[j];
j -=gap;
}
arr[j+gap]=temp;
}
gap /=2;
}
return arr;
}
5.归并排序
归并排序 是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法步骤
步骤1:把长度为n的输入序列分成两个长度为n/2的子序列;
步骤2:对这两个子序列分别采用归并排序;
步骤3:将两个排序好的子序列合并成一个最终的排序序列。
//归并排序 O(nlogn) 稳定 需要O(n)的额外空间
//归并操作部分
void merge(vector<int> arr,int low,int mid,int high){
int i=low,j=mid+1;
vector<int> temp;
int t=0;
while(i<=mid &&j<=high){
if(arr[i]<arr[j]){
temp[t++]=arr[i++];
}
else{
temp[t++]=arr[j++];
}
}
while(i<=mid){
temp[t++]=arr[i++];
}
while(j<=high){
temp[t++]=arr[j++];
}
t=0;
while(low<=high){
arr[low++]=temp[t++];
}
}
void mergeSort(vector<int> arr,int low,int high){
if(low<high){
int mid=low+(high-low)/2;
//左子数组融合排序
mergeSort(arr, low, mid);
//右子数组融合排序
mergeSort(arr, mid + 1, high);
//已经排序好的子数组有序融合
merge(arr, low, mid, high);
}
}
public class MergeSort {
// 非递归式的归并排序
public static int[] mergeSort(int[] arr) {
int n = arr.length;
// 子数组的大小分别为1,2,4,8...
// 刚开始合并的数组大小是1,接着是2,接着4....
for (int i = 1; i < n; i += i) {
//进行数组进行划分
int left = 0;
int mid = left + i - 1;
int right = mid + i;
//进行合并,对数组大小为 i 的数组进行两两合并
while (right < n) {
// 合并函数和递归式的合并函数一样
merge(arr, left, mid, right);
left = right + 1;
mid = left + i - 1;
right = mid + i;
}
// 还有一些被遗漏的数组没合并,千万别忘了
// 因为不可能每个字数组的大小都刚好为 i
if (left < n && mid < n) {
merge(arr, left, mid, n - 1);
}
}
return arr;
}
}
6.快速排序
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
步骤1:从数列中挑出一个元素,称为 “基准”(pivot );
步骤2:重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
步骤3:递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
//快速排序 O(nlogn) 不稳定
void swap(vector<int>& arr,int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
int partion(vector<int> arr,int low,int high){
int privot=arr[low]; //以第一个数据作为基准
while(low<high){
while((low<high)&&arr[low]<=privot){
low++;
}
swap(arr,low,high);
while(low<high && array[high]>=privot){
high--;
}
swap(arr,low,high);
}
return low; //基准数据在数组中应该存在的位置
}
void quickSort(vector<int> arr;int low,int high){
if(low<high){
int index=partion(array,low,high);
quickSort(arr,low,index-1);
quickSort(arr,index+1,high);
}
}
当数据量很小(N<=20)时,快速排序效果不如插入排序,因为快速排序不稳定且有递归开销
7.堆排序
堆排序的基本思路:
堆排序的基本思想是:将待排序序列构造成一个大顶堆(升序),此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
步骤1:将待排序序列构造成一个大顶堆,从最后一个非叶子节点开始调整,最后一个非叶子节点i=(n-2)/2,然后i–(i为相邻的非叶子节点)
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换
//时间复杂度O(nlogn) 不稳定
class Head{
public:
int headsort(vector<int> arr){
int n=arr.size();
//首先将待排序的数组构建成大顶堆
//从最后一个非叶子节点开始操作
for(int i=(n-2)/2;i>=0;i--){
downAdjust(arr,i,n-1);
}
//步骤二 不断将堆顶元素与末尾元素进行交换
for(int i=n-1;i>=0;i--){
int temp=arr[i];
arr[i]=arr[0];
arr[0]=temp;
//调整打乱的堆
downAdjust(arr,0;i-1); //i-1既是将末尾元素放置,因为此时末尾元素为最大值
}
return arr;
}
//元素下沉操作,从根节点开始调整堆
void downAdjust(vector<int> arr,int parent,int n){
int temp=arr[parent];
//先定位到其左节点
int child=2*parent-1;
while(child<=n){
//如果右节点>左节点,则定位到右节点
if(child+1<=n&&(arr[child]<arr[child+1])){
child++;
}
if(arr[child]<temp){break;}//满足要求,不必调整
arr[parent]=arr[child] //互换
arr[child]=temp;
parent=child; //将该节点作为父节点,继续调整
child=2*parent-1;
}
arr[parent]=temp;
}
};
8.计数排序
计数排序(Counting sort) 是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对有确定范围的整数进行排序。
8.1 算法描述
步骤1:找出待排序的数组中最大和最小的元素;
步骤2:统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
步骤3:对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
步骤4:反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
//时间复杂度O(n+k),空间复杂度O(k) 稳定
int countingSort(vector<int> arr){
int min=arr[0],max=arr[0];
for(int i=1;i<arr.size();i++){
if(arr[i]<min){min=arr[i];}
if(arr[i]>max){max=arr[i];}
}
int bucketlen=max-min+1;
//int bucketmin=0-min;
//优化版本取最大最小值的差,所以构造的时候需要减偏移量min,最后在加上
vector<int> bucket(bucketlen,0);
for(int i=0;i<bucketlen;i++){
bucket[arr[i]-min]++;
}
int i=0;
for(int j=0;j<bucketlen;j++){
while(buckec[j]>0){
arr[i++]=j+min;
bucket[j]--;
}
}
return arr;
}
算法分析:计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
9.桶排序
桶排序 是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排
算法描述
步骤1:人为设置一个BucketSize,作为每个桶所能放置多少个不同数值(例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,但是容量不限,即可以存放100个3);
步骤2:遍历输入数据,并且把数据一个一个放到对应的桶里去;
步骤3:对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;
步骤4:从不是空的桶里把排好序的数据拼接起来。
//稳定 时间复杂度O(n+k),空间复杂度O(n+k)
void bucketSort(vector<int> arr,int bucketsize){ //设置一个桶装几个数
int min=arr[0],max=arr[0];
for(int i=1;i<arr.size();i++){
if(arr[i]<min){min=arr[i];}
if(arr[i]>max){max=arr[i];}
}
int bucketcount=(max-min+1)/bucketsize; //计算需要几个桶
vector<vector<int>> buckets(bucketcount);
for(int i=0;i<arr.size();i++){
int index=(arr[i]-min)/bucketsize;
buckets[index].push_back(arr[i]);
}
int index=0;
for(vector<int> bucket:buckets){
if(!bucket.empty()){
quickSort(bucket); //选择一个排序方式给桶中数据排序
for(auto num:bucket){
arr[index++]=num;
}
}
}
}
稳定算法;
常见排序算法中最快的一种;
适用于小范围(最大值和最小值差值较小),独立均匀分布的数据;
可以计算大批量数据,符合线性期望时间;
外部排序方式,需额外耗费n个空间;
10.基数排序
基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;
算法步骤:
1.将各待比较元素数值统一数位长度,即对数位短者在前补零;
2.根据个位数值大小,对数组进行排序;
3.重复上一步骤,依次根据更高位数值进行排序,直至到达最高位;
//时间:O(kn) 空间O(k+n)
public class RadioSort {
public static int[] radioSort(int[] arr) {
if(arr == null || arr.length < 2) return arr;
int n = arr.length;
int max = arr[0];
// 找出最大值
for (int i = 1; i < n; i++) {
if(max < arr[i]) max = arr[i];
}
// 计算最大值是几位数
int num = 1;
while (max / 10 > 0) {
num++;
max = max / 10;
}
// 创建10个桶
ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(10);
//初始化桶
for (int i = 0; i < 10; i++) {
bucketList.add(new LinkedList<Integer>());
}
// 进行每一趟的排序,从个位数开始排
for (int i = 1; i <= num; i++) {
for (int j = 0; j < n; j++) {
// 获取每个数最后第 i 位是数组
int radio = (arr[j] / (int)Math.pow(10,i-1)) % 10;
//放进对应的桶里
bucketList.get(radio).add(arr[j]);
}
//合并放回原数组
int k = 0;
for (int j = 0; j < 10; j++) {
for (Integer t : bucketList.get(j)) {
arr[k++] = t;
}
//取出来合并了之后把桶清光数据
bucketList.get(j).clear();
}
}
return arr;
}
}
稳定算法;
适用于正整数数据(若包含负数,那么需要额外分开处理);
对于实数,需指定精度,才可使用此算法。
8,9,10三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
基数排序: 根据键值的每位数字来分配桶
计数排序: 每个桶只存储单一键值
桶排序: 每个桶存储一定范围的数值