归并排序和快速排序为一类(使用递归);
堆排序:优先队列,在leetcode刷题中使用较多;
计数排序、桶排序和基数排序为一类(使用累加数组);
我看了很多博客,对于计数排序和基数排序都没有清晰的讲解。在理解如何之后来给大家分享。如果存在错误,请多包涵。
代码实现后,可以使用leetcode 912题数组排序来检验正确性。912. 数组排序
算法分类
十种常见排序算法可以分为两大类:
算法复杂度
相关概念
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。先使每个子序列有序,再使子序列段间有序。
递归(递去归来):递去分,归来并。因为当只有一个元素时,一定有序。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
5.1 算法描述
5.2 动图演示
5.3 代码实现
归并排序——将两段排序好的数组结合成一个排序数组
思路:将数组一分为二,分别进行排序,但是如果数组中元素大于1个无法进行直接判断(因此递归划分直到子序列只有一个元素)。
要保证排好序,需要从最少个数(一个)进行归并,满足两个有序序列才能正确合并。
#include
#include
#include
class Solution {
vector<int> temp;
public:
vector<int> sortArray(vector<int>& nums) {
if (nums.size() < 2) return nums;
temp.resize(nums.size()); //注意要开辟空间,否则无法使用[]
mergeSort(nums,0,nums.size()-1);
return nums;
}
//两两合并需要左中右三个指针进行区间划分(left,mid) 和 (mid+1,right)
void merge(vector<int>& arr, int l, int mid, int r) {
//左区间的开始,右区间开始,记录数组的开始
int i = l, j = mid + 1, k = 0;
//合并两个区间:都存在元素和一个区间已经为空
while (i <= mid && j <= r) {
if (arr[i] <= arr[j]) temp[k++] = arr[i++];
else temp[k++] = arr[j++];
}
while (i <= mid) temp[k++] = arr[i++];
while (j <= r) temp[k++] = arr[j++];
//覆盖到原来的数组,合并的区间为l~r,拷贝也是一样
for (int i = l, j = 0; i <= r; i++, j++) arr[i] = temp[j];
}
void mergeSort(vector<int>& arr, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
};
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
6.1 算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
6.2 动图演示
6.3 代码实现
有序的时候最坏,每次的基准数不能很好的二分。
(1)先找一个基准数,(比如设置最左为基准数:arr[left])所有数都与其比较;
(2)将当前分区的左右进行记录(i=left,j=right),不记录后面无法定位下一个分区;
(3)挖坑填数:while(i (4)通过绘图分析发现,基准数将左右两边进行了分区,利用递归实现排序—递归(注意终止条件) 与归并算法的区别就在于:快排为递去的过程解决问题,归并在归来的过程出来问题。 堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。 7.1 算法描述 7.2 动图演示 7.3 递归代码实现 堆:完全二叉树(结点从左到右加入,可用数组表示)。大顶堆:每个结点的值都大于或者等于其左右子树的值,根就是最大的值。用于升序和降序排序。如何排序:由于是完全二叉树,满足左结点N[2i+1],右结点N[2i+2],根结点N[(i-1)/2]。 如何根据二叉树调整为堆 (1)初始化堆:先从倒数第二层开始判断是否较大,将较大的往上交换,直到交换到根结点; (2)根结点下沉:将根和最后一个元素交换,然后重复上面的交换过程。最后最大的元素就会在最后一个,然后将倒数第二和根交换,重复直到排好序。 因为为完全二叉树,知道结点和数组下标的位置关系:2i+1; 2i+2; (i-1)/2 计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数(比如按照年龄进行排序)。 计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。 8.1 算法描述 8.2 动图演示 8.3 代码实现 8.4 图解累加数组原理 桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。 桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。 9.1 算法描述 9.2 图片演示 9.3 代码实现 (1)初始化桶,利用最大最小值计算需要多少个桶 (count=(max-min)/n+1) (2)利用均匀分布的思想进行映射到需要存放的桶(k=(arr[i]-min)/n) (3)对每一个桶进行排序,然后进行拼接。(insertSort之后拼接(insert)) 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。 按照个位分配,按照十位分配(当实现按照十位分配,可以看到十位数不同的已经实现了排序)然后重复分配重复收集。因此收集分配的次数和最大值的位数相同。 基数排序的空间复杂度为O(n+k),其中k=10为桶的数量。一般来说n>>k。 10.1 算法描述 10.2 动图演示 10.3 代码实现 以上动画其实也无法理解 基数排序的具体实现。基数排序就是在计数排序的思想上进行拓展(计数排序需要的桶为min—max),而基数排序按照每一位的取值(0~9)进行排序只需要10个桶即可。弄懂了计数排序,基数排序也就明了很多了。 重点是理解计数排序的累加数组。 两层循环,挨着判断是否需要交换。算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。 1.1 算法描述 1.2 动图演示 2.1 算法描述 选择排序是不稳定的排序算法,例如 2 2 1。因为第一个2和1进行交换后;2,2的相对位置改变了。 选择排序数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。 n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下: 2.2 动图演示 2.3 代码实现 初始化最小下标的位置,遇到较小的即更换下标。检索到最后一个时进行交换。一趟之后,有序区1个元素,无序区n-1个元素。重复上述过程n-1趟即可排序完成。 基本有序 or 数据量少,插入效率高 std::sort原理:对要排序的元素数目有一个阈值,如果大于该阈值则是用快速排序,如果小于阈值则用插入排序。 工作原理:构建有序序列,将未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 3.1 算法描述 一般来说,插入排序都采用in-place(O(1)空间)在数组上实现。具体算法描述如下: 3.2 动图演示 3.2 代码实现 1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。 4.1 算法描述 将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述: 4.2 代码实现 先设置一个增量,然后将数据进行分组,在每个分组进行插入排序。减小增量,重复排序,直到增量为1。初始选择增量gap=length/2;在每一个分割的子序列中进行插入排序;缩小增量继续以gap = gap/2的方式。 但是算法推荐增量为 :h(i+1)=3h(i)+1,也就是incr=length, incr=incr/3+1。 两层for循环:第一层是增量变化;第二层(增量为1,可以将元素分到每一个小组)在每个分组内(进行下标的切换即可)进行插入排序。 参考:十大经典排序算法(动图演示)//使用挖坑填数法:
//先后往前移动,才能从前往后寻找。
void quicksort(vector<int>& nums, int l, int r){
if (l >= r) return;
int i = l, j = r;
int pivot = rand() % (r - l + 1) + l;
swap(nums[l],nums[pivot]);
//int pivot = nums[l];//不用定义,否则容易出现覆盖
while (i < j) {
while (i < j && nums[j] >= nums[l]) j--;
while (i < j && nums[i] <= nums[l]) i++;
swap(nums[i], nums[j]);
}
swap(nums[i],nums[l]);
quicksort(nums, l, i-1);
quicksort(nums, i+1, r);
}
7、堆排序(Heap Sort)
//-----堆排序:根据数组下标和节点的对应关系:father = son * 2 + 1 和 son * 2 + 2;
void HeapSort(vector<int>& arr) {
int n = arr.size() - 1; // n为最后一个下标
//(1)初始化大顶堆:从最后一个父节点开始进行调整
for (int i = n / 2; i >= 0; i--) {
heapify(arr, i, n);
}
//(2)堆排序:交换堆顶元素和当前最后第一个元素
for (int i = n; i > 0; i--) {
swap(arr[0], arr[i]);
n -= 1;
heapify(arr, 0, n);
}
}
// 递归
void heapify(vector<int>& arr, int start, int end) {
if (start >= end) return;
int father = start, son = father * 2 + 1;
if (son > end) return; //左不存在
if (son + 1 <= end && arr[son] < arr[son + 1]) son++; //左右都存在需要比较
if (arr[son] <= arr[father]) return; //如果父节点大于孩子,直接返回
else swap(arr[son], arr[father]); //交换之后继续向下判断
heapify(arr, son, end);
}
//迭代
void heapify2(vector<int>& arr, int start, int end) {
int father = start, son = father * 2 + 1;
//当存在孩子就需要进行比较
while (son <= end) {
if (son + 1 <= end && arr[son] < arr[son + 1]) son++;
if (arr[son] <= arr[father]) break;
else swap(arr[son], arr[father]);
father = son;
son = father * 2 + 1;
}
}
8、计数排序(Counting Sort)
void countSort(vector<int>& arr){
int amax=*max_element(arr.begin(),arr.end());
//统计元素出现的次数
vector<int> countarr((amax+1),0); //拓展:考虑不从0开始如何计数
for(auto a:arr){
countarr[a]++;
}
//重新构建arr
arr.clear();
for(int i=0;i<amax+1;i++){
while((i<amax+1) && countarr[i]!=0){
arr.push_back(i);
countarr[i]--;
}
}
}
//以上方法不稳定,如何调整-->使用累加数组
void countSort1(vector<int>& arr) {
int n = arr.size();
int max = *max_element(arr.begin(), arr.end());
vector<int> count(max + 1, 0); //从0开始需要max+1个位置
for (auto x : arr) count[x]++; //统计出现次数
//保证稳定性,使用累加数组
for (int i = 1; i < count.size(); i++) count[i] += count[i - 1];
//根据累加数组 反向重建数组
vector<int> temp(n);
for (int i = n - 1; i >= 0; i--) {
//找到arr[i]在哪一个位置,根据count就可以知道存放的位置。用前先--,对应存放位置
temp[--count[arr[i]]] = arr[i];
}
arr.assign(temp.begin(), temp.end());
}
9、桶排序(Bucket Sort)
void bucketSort(vector<int>& arr) {
//初始化桶,桶的个数及开辟空间
int aminid = 0, amaxid = 0;
int n = arr.size();
for (int i = 1; i < n; i++) {
if (arr[i] > arr[amaxid]) amaxid = i;
if (arr[i] < arr[aminid]) aminid = i;
}
int amax = arr[amaxid], amin = arr[aminid];
int count = (amax - amin) / n + 1;
vector<vector<int>> bucket(count, vector<int>());
//将元素映射到每个桶中,利用均匀分布的思想进行映射
for (int i = 0; i < n; i++) {
int k = (arr[i]-amin)/ n;//映射到 需要放在哪一个桶
bucket[k].push_back(arr[i]);//放入桶中
}
arr.clear();
//对每个桶进行排序(插入排序),然后拼接
for (int i = 0; i < count; i++) {
insertSort(bucket[i]);
arr.insert(arr.end(),bucket[i].begin(),bucket[i].end());
}
}
//插入排序的思想:将无序序列插入到有序序列中,先记录插入位置最后再插入
void insertSort(vector<int> arr){
int n=arr.size();
for(int i=1;i<n;i++) //第一个当作有序序列,从第二个开始
{
int pre=i-1; //记录当前有序序列最后一个
int cur=arr[i]; //记录当前需要进行插入的数字
//先移动后插入
while(pre>=0 && cur<arr[pre]){
arr[pre+1]=arr[pre];
pre--;
}
arr[pre+1]=cur;
}
}
10、基数排序(Radix Sort)
void radixSort(vector<int>& arr) {
int n = arr.size();
//计算最大值的位数
int max = *max_element(arr.begin(), arr.end());
int digit = 0;
int base = 1;
while (max / base > 0) {
digit++;
base *= 10;
}
//需要排序的次数=位数
base = 1;
for (int i = 0; i < digit; i++) {
//统计出现次数
int bucket[10] = { 0 };
for (int j = 0; j < n; j++)
bucket[(arr[j] / base) % 10]++;
//使用累加数组
for (int j = 1; j < 10; j++)
bucket[j] += bucket[j - 1];
//根据累加数组 反向重建数组
vector<int> temp(n);
for (int k = n - 1; k >= 0; k--) {
temp[--bucket[(arr[k] / base) % 10]] = arr[k];
}
//将排好序的元素覆盖到原数组
arr.assign(temp.begin(), temp.end());
base *= 10;
}
}
1、冒泡排序(Bubble Sort,稳定)
void BubbleSort(vector<int>& nums) {
//common
int n = nums.size();
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (nums[j] > nums[j + 1])
swap(nums[j], nums[j + 1]);
}
}
//优化1:如果某一轮中没有出现交换,说明已经有序
int n = nums.size();
for (int i = 0; i < n - 1; i++) {
bool flag = true;
for(int j = 0; j < n - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
swap(nums[j], nums[j + 1]);
flag = false;
}
}
if (flag) break;
}
//优化二:如果前半部分无序后半部分有序,可以每次记录最后交换的位置
int n = nums.size();
int lastpos = n - 1;
for (int i = 0; i < n - 1; i++) {
int k = lastpos;
bool flag = true;
for (int j = 0; j < k; j++) {
if (nums[j] > nums[j + 1]) {
swap(nums[j], nums[j + 1]);
flag = false;
lastpos = j;
}
}
if (flag) break;
}
return nums;
}
2、选择排序
void SelectSort(vector<int>& nums) {
int n = nums.size();
//选择排序:选择最小的元素,记录最小元素下标后进行交换
for (int i = 0; i < n - 1; i++) {
int idx = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[idx])
idx = j;
}
if (idx != i) swap(nums[i],nums[idx]);
}
}
3、插入排序(Insertion Sort,稳定)
//将第一个看成有序列;下一个无序的元素找合适的位置(边找边移动);最后再插入。
//-------------插入排序----------------
void InsertSort(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n - 1; i++) {
int pre = i;
//当前元素和前面有序区间元素比较并交换,然后继续向前比较
while (pre >= 0 && nums[pre + 1] < nums[pre]) {
swap(nums[pre+1], nums[pre]);
pre--;
}
}
}
4、希尔排序(Shell Sort)
//--------------希尔排序------------
//增量控制避免最坏情况,注意插入中的操作都是 j+incr 或者 j-incr
void ShellSort(vector<int>& nums) {
int n = nums.size();
for (int incr = n / 2; incr > 0; incr = incr / 2) {
//进行插入排序
//i++是关键;incr前的元素都是单个分组中有序序列,将后面的无序序列找到对应的分组进行插入排序
for (int j = incr; j < n; j++) {
int pre = j - incr;
while (pre >= 0 && nums[pre] > nums[pre + incr]) {
swap(nums[pre], nums[pre + incr]);
pre -= incr;
}
}
}
}