【基础算法】(04) 十二种排序算法(第四篇)

2024-06-06 17:58

本文主要是介绍【基础算法】(04) 十二种排序算法(第四篇),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

【基础算法】(04) 十二种排序算法(第四篇)

Auther: Thomas Shen
E-mail: Thomas.shen3904@qq.com
Date: 2017/10/21
All Copyrights reserved !

      • 基础算法04 十二种排序算法第四篇
        • 篇述
        • 直接插入排序 Straight Insertion Sort
        • 二分插入排序 Binary insert sort
        • 希尔排序 Shells Sort
        • 选择排序简单选择排序Simple Selection Sort
        • 选择排序堆排序Heap Sort
        • 交换排序冒泡排序Bubble Sort
        • 鸡尾酒排序双向冒泡排序
        • 交换排序快速排序Quick Sort
        • 归并排序Merge Sort
        • 桶排序 Bucket sort
        • 计数排序 Counting sort
        • 基数排序 Radix Sort
        • 总结
        • References


1. 篇述:

本系列总结了常用的十二种排序算法,每个算法都包括算法原理, 代码实现, 面试例题 三部分。

其中本文是排序算法系列的第三篇,介绍了:

  • 11. 桶排序 (Bucket sort),
  • 12. 计数排序 (Counting sort),
  • 13. 基数排序 (Radix Sort),

2. 直接插入排序 (Straight Insertion Sort):
3. 二分插入排序 (Binary insert sort):
4. 希尔排序 (Shell’s Sort):
5. 选择排序—简单选择排序(Simple Selection Sort):
6. 选择排序—堆排序(Heap Sort):
7. 交换排序—冒泡排序(Bubble Sort):
8. 鸡尾酒排序/双向冒泡排序:
9. 交换排序—快速排序(Quick Sort):
10. 归并排序(Merge Sort):

参见第一、二、三篇;


11. 桶排序 (Bucket sort):

桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。

桶排序是稳定的,且在大多数情况下常见排序里最快的一种,比快排还要快,缺点是非常耗空间,基本上是最耗空间的一种排序算法,而且只能在某些情形下使用。

11.1 原理简介:
桶排序具体算法描述如下:

  1. 设置一个定量的数组当作空桶子。
  2. 寻访串行,并且把项目一个一个放到对应的桶子去。
  3. 对每个不是空的桶子进行排序。
  4. 从不是空的桶子里把项目再放回原来的串行中。

桶排序最好情况下使用线性时间O(n),很显然桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为 其它部分的时间复杂度都为O(n);很显然,桶划分的越小,各个桶之间的数据越少,排 序所用的时间也会越少。但相应的空间消耗就会增大。

可以证明,即使选用插入排序作为桶内排序的方法,桶排序的平均时间复杂度为线性。 具体证明,请参考算法导论。其空间复杂度也为线性。

桶排序是一种很巧妙的排序方法,在处理密集型数排序的时候有比较好的效果(主要是这种情况下空间复杂度不高)

11.2 代码实现:

#include <time.h>  
#include <iostream>  
#include <iomanip>  
using namespace  std;  /*initial arr*/  
void InitialArr(double *arr,int n)  
{  srand((unsigned)time(NULL));  for (int i = 0; i<n;i++)  {  arr[i] = rand()/double(RAND_MAX+1);   //(0.1)  }  
}  /* print arr*/  
void PrintArr(double *arr,int n)  
{  for (int i = 0;i < n; i++)  {  cout<<setw(15)<<arr[i];  if ((i+1)%5 == 0 || i ==  n-1)  {  cout<<endl;  }  }  
}  void BucketSort(double * arr,int n)       
{  double **bucket = new double*[10];  for (int i = 0;i<10;i++)  {  bucket[i] = new double[n];  }  int count[10] = {0};  for (int i = 0 ; i < n ; i++)  {  double temp = arr[i];  int flag = (int)(arr[i]*10); //flag标识小树的第一位   bucket[flag][count[flag]] = temp; //用二维数组的每个向量来存放小树第一位相同的数据  int j = count[flag]++;  /* 利用插入排序对每一行进行排序 */  for(;j > 0 && temp < bucket[flag][j - 1]; --j)  {  bucket[flag][j] = bucket[flag][j-1];  }  bucket[flag][j] =temp;  }  /* 所有数据重新链接 */  int k=0;  for (int i = 0 ; i < 10 ; i++)  {  for (int j = 0 ; j< count[i];j++)  {  arr[k] = bucket[i][j];  k++;  }  }  for (int i = 0 ; i<10 ;i++)  {  delete bucket[i];  bucket[i] =NULL;  }  delete []bucket;  bucket = NULL;  
}  void main()  
{  double *arr=new double[10];  InitialArr(arr, 10);  BucketSort(arr, 10);  PrintArr(arr,10);  delete [] arr;  
}  

11.3 面试例题:
11.3.1 例题一:

一年的全国高考考生人数为500 万,分数使用标准分,最低100 ,最高900 ,没有小数,你把这500 万元素的数组排个序。

对500W数据排序,如果基于比较的先进排序,平均比较次数为O(5000000*log5000000)≈1.112亿。但是我们发现,这些数据都有特殊的条件: 100 =< score <= 900。那么我们就可以考虑桶排序这样一个“投机取巧”的办法、让其在毫秒级别就完成500万排序。

创建801(900-100)个桶。将每个考生的分数丢进f(score)=score-100的桶中。这个过程从头到尾遍历一遍数据只需要500W次。然后根据桶号大小依次将桶中数值输出,即可以得到一个有序的序列。而且可以很容易的得到100分有xxx人,501分有xxx人。

实际上,桶排序对数据的条件有特殊要求,如果上面的分数不是从100-900,而是从0-2亿,那么分配2亿个桶显然是不可能的。所以桶排序有其局限性,适合元素值集合并不大的情况。

11.3.2 例题二:
在一个文件中有 10G 个整数,乱序排列,要求找出中位数。内存限制为 2G。只写出思路即可(内存限制为 2G的意思就是,可以使用2G的空间来运行程序,而不考虑这台机器上的其他软件的占用内存)。

分析: 既然要找中位数,很简单就是排序的想法。那么基于字节的桶排序是一个可行的方法。

按以下步骤实施:

  1. 把10G整数每2G读入一次内存,然后一次遍历这536,870,912即(1024*1024*1024)*2 /4个数据。每个数据用位运算”>>”取出最高8位(31-24)。这8bits(0-255)最多表示255个桶,那么可以根据8bit的值来确定丢入第几个桶。最后把每个桶写入一个磁盘文件中,同时在内存中统计每个桶内数据的数量,自然这个数量只需要255个整形空间即可。
  2. 继续以内存中的整数的次高8bit进行桶排序(23-16)。过程和第一步相同,也是255个桶。
  3. 一直下去,直到最低字节(7-0bit)的桶排序结束。我相信这个时候完全可以在内存中使用一次快排就可以了。

11.3.3 例题三:
给定n个实数x1,x2,…,xn,求这n个实数在实轴上相邻2个数之间的最大差值M,要求设计线性的时间算法
典型的最大间隙问题。

要求线性时间算法。需要使用桶排序。桶排序的平均时间复发度是O(N).如果桶排序的数据分布不均匀,假设都分配到同一个桶中,最坏情况下的时间复杂度将变为O(N^2).

桶排序: 最关键的建桶,如果桶设计得不好的话桶排序是几乎没有作用的。通常情况下,上下界有两种取法,第一种是取一个10^n或者是2^n的数,方便实现。另一种是取数列的最大值和最小值然后均分作桶。

对于这个题,最关键的一步是:由抽屉原理知:最大差值M>= (Max(V[n])-Min(V[n]))/(n-1)!所以,假如以(Max(V[n])-Min(V[n]))/(n-1)为桶宽的话,答案一定不是属于同一个桶的两元素之差。因此,这样建桶,每次只保留桶里面的最大值和最小值即可。

代码如下:

//距离平均值为offset = (arrayMax - arrayMin) / (n - 1), 则距离最大的数必然大于这个值  
//每个桶只要记住桶中的最大值和最小值,依次比较上一个桶的最大值与下一个桶的最小值的差值,找最大的即可.  
#include <iostream>  
#define MAXSIZE 100    //实数的个数  
#define MAXNUM 32767  
using namespace std;  
struct Barrel  
{  double min;   //桶中最小的数  double max;   //桶中最大的数  bool flag;   //标记桶中有数  
};  
int BarrelOperation(double* array, int n)  
{  Barrel barrel[MAXSIZE];  //实际使用的桶  int nBarrel = 0;  //实际使用桶的个数  Barrel tmp[MAXSIZE];   //临时桶,用于暂存数据  double arrayMax = -MAXNUM, arrayMin = MAXNUM;  for(int i = 0; i < n; i++) {  if(array[i] > arrayMax)  arrayMax = array[i];  if(array[i] < arrayMin)  arrayMin = array[i];  }  double offset = (arrayMax - arrayMin) / (n - 1);  //所有数的平均间隔  //对桶进行初始化  for(i = 0; i < n; i++) {    tmp[i].flag = false;  tmp[i].max = arrayMin;  tmp[i].min = arrayMax;  }  //对数据进行分桶  for(i = 0; i < n; i++) {     int pos = (int)((array[i] - arrayMin) / offset);  if(!tmp[pos].flag) {  tmp[pos].max = tmp[pos].min = array[i];  tmp[pos].flag = true;  } else {  if(array[i] > tmp[pos].max)  tmp[pos].max = array[i];  if(array[i] < tmp[pos].min)  tmp[pos].min = array[i];  }   }  for(i = 0; i <= n; i++) {  if(tmp[i].flag)   barrel[nBarrel++] = tmp[i];     }  int maxOffset = 0.0;  for(i = 0; i < nBarrel - 1; i++) {  if((barrel[i+1].min - barrel[i].max) > maxOffset)   maxOffset = barrel[i+1].min - barrel[i].max;  }  return maxOffset;  
}  
int main()  
{  double array[MAXSIZE] = {1, 8, 6, 11, 7, 13, 16, 5};  //所需处理的数据  int n = 8; //数的个数  //double array[MAXSIZE] = {8, 6, 11};  //int n = 3;  int maxOffset = BarrelOperation(array, n);  cout << maxOffset << endl;  return 0;  
}  

12. 计数排序 (Counting sort):

12.1 原理简介:
计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。

算法的步骤如下:
1. 找出待排序的数组中最大和最小的元素;
2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。
计数排序不是比较排序,排序的速度快于任何比较排序算法。

由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。

12.2 代码实现:

#include <stdlib.h>  
#include <string.h>  
#include <stdio.h>  
/************************************************************** 功能:计数排序。 参数: data : 要排序的数组 size :数组元素的个数 k   :数组中元素数组最大值 +1 (这个需要+1) 返回值: 成功0;失败-1.        *************************************************************/  
int ctsort(int *data, int size, int k)  
{  int * counts = NULL,/*计数数组*/  * temp = NULL;/*保存排序后的数组*/  int i = 0;  /*申请数组空间*/  if ((counts = (int *) malloc( k * sizeof(int))) == NULL)  return -1;  if ((temp = (int *) malloc( k * sizeof(int))) == NULL)  return -1;  /*初始化计数数组*/  for (i = 0; i < k; i ++)  counts[i] = 0;  /*数组中出现的元素,及出现次数记录*/  for(i = 0; i < size; i++)  counts[data[i]] += 1;  /*调整元素计数中,加上前一个数*/  for (i = 1; i < k; i++)  counts[i] += counts[i - 1];  /*使用计数数组中的记录数值,来进行排序,排序后保存的temp*/  for (i = size -1; i >= 0; i --){  temp[counts[data[i]] - 1] = data[i];  counts[data[i]] -= 1;  }  memcpy(data,temp,size * sizeof(int));  free(counts);  free(temp);  return 0;  
}  
int main()  
{  int a[8] = {2,0,2,1,4,6,7,4};  int max = a[0],  i = 0;  /*获得数组中中的数值*/  for ( i = 1; i < 8; i++){  if (a[i] > max)  max = a[i];  }  ctsort(a,8,max+1);  for (i = 0;i < 8;i ++)  printf("%d\n",a[i]);  
}  

13. 基数排序 (Radix Sort):

13.1 原理简介:

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

算法过程描述如下:

  1. 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
  2. 从最低位开始,依次进行一次排序。
  3. 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列

基数排序的时间复杂度是 O(k•n),其中n是排序元素个数,k是数字位数。

注意这不是说这个时间复杂度一定优于O(n·log(n)),因为k的大小一般会受到n的影响。 以排序n个不同整数来举例,假定这些整数以B为底,这样每位数都有B个不同的数字,k就一定不小于logB(n)。由于有B个不同的数字,所以就需要B个不同的桶,在每一轮比较的时候都需要平均n·log2(B) 次比较来把整数放到合适的桶中去,所以就有:
k 大于或等于 logB(n)
每一轮(平均)需要 n·log2(B) 次比较
所以,基数排序的平均时间T就是:
T>=logB(n)·n·log2(B) = log2(n)·logB(2)·n·log2(B) = log2(n)·n·logB(2)·log2(B) = n·log2(n)
所以和比较排序相似,基数排序需要的比较次数:T ≥ n·log2(n)。 故其时间复杂度为 Ω(n·log2(n)) = Ω(n·log n)

这里写图片描述

13.2 代码实现:

#include <stdio.h>   
#include <stdlib.h>   
void radixSort(int data[]) {  int temp[10][10] = {0};   int order[10] = {0};   int n = 1;   while(n <= 10) {   int i;  for(i = 0; i < 10; i++) {   int lsd = ((data[i] / n) % 10);   temp[lsd][order[lsd]] = data[i];   order[lsd]++;   }   // 重新排列  int k = 0;  for(i = 0; i < 10; i++) {   if(order[i] != 0)  {  int j;  for(j = 0; j < order[i]; j++, k++) {   data[k] = temp[i][j];   }   }  order[i] = 0;   }   n *= 10;   }       
}  
int main(void) {   int data[10] = {73, 22, 93, 43, 55, 14, 28, 65, 39, 81};   printf("\n排序前: ");   int i;  for(i = 0; i < 10; i++)   printf("%d ", data[i]);   putchar('\n');   radixSort(data);  printf("\n排序後: ");   for(i = 0; i < 10; i++)   printf("%d ", data[i]);   return 0;   
}   

13.3 面试例题:

计数排序在处理密集整数排序的问题的时候非常有限,尤其是有时候题目对空间并不做太大限制,那使用计数排序能够达到O(n)的时间复杂度,远快于所有基于比较的其他排序方法。


14. 总结:

这里写图片描述


References. :
  • [ 1 ]. Coursera | Java程序设计 | PKU
  • [ 2 ]. 转载自:八大排序算法
  • [ 3 ]. 转载自:12种排序算法详解

这篇关于【基础算法】(04) 十二种排序算法(第四篇)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1036823

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

usaco 1.3 Mixing Milk (结构体排序 qsort) and hdu 2020(sort)

到了这题学会了结构体排序 于是回去修改了 1.2 milking cows 的算法~ 结构体排序核心: 1.结构体定义 struct Milk{int price;int milks;}milk[5000]; 2.自定义的比较函数,若返回值为正,qsort 函数判定a>b ;为负,a<b;为0,a==b; int milkcmp(const void *va,c

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

poj 3974 and hdu 3068 最长回文串的O(n)解法(Manacher算法)

求一段字符串中的最长回文串。 因为数据量比较大,用原来的O(n^2)会爆。 小白上的O(n^2)解法代码:TLE啦~ #include<stdio.h>#include<string.h>const int Maxn = 1000000;char s[Maxn];int main(){char e[] = {"END"};while(scanf("%s", s) != EO

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

hdu 1285(拓扑排序)

题意: 给各个队间的胜负关系,让排名次,名词相同按从小到大排。 解析: 拓扑排序是应用于有向无回路图(Direct Acyclic Graph,简称DAG)上的一种排序方式,对一个有向无回路图进行拓扑排序后,所有的顶点形成一个序列,对所有边(u,v),满足u 在v 的前面。该序列说明了顶点表示的事件或状态发生的整体顺序。比较经典的是在工程活动上,某些工程完成后,另一些工程才能继续,此时