最近为了熟悉经典的排序算法,我又进行了复习总结,实现过程全部由C++完成。代码结构清晰简洁,方便学习和复习,也是我们必须掌握的基础。
算法基本知识
什么是稳定排序、原地排序、时间复杂度、空间复杂度:
1、稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 仍然在 b 的前面,则为稳定排序。
2、非稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 可能不在 b 的前面,则为非稳定排序。
3、原地排序:原地排序就是指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。
4、非原地排序:需要利用额外的数组来辅助排序。
5、时间复杂度:一个算法执行所消耗的时间。
6、空间复杂度:运行完一个算法所需的内存大小
1、冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调,比较是相邻的两个元素,再进行交换。
- 冒泡排序是稳定的
- 如果两个元素相等,不会把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
算法过程
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
2、选择排序
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给>二个元素选择第二小的,依次类推,直到第n-1个元素,第n个 元素不用选择了,因为只剩下它一个最大的元素了。
- 选择排序是非稳定的
- 在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么 交换后稳定性就被破坏了。
算法过程
- 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
- 以此类推,直到所有元素均排序完毕
- 时间负复杂度:O(n^2),空间O(1),非稳定排序,原地排序
3、插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开 始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。
- 如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
算法过程
1.从第一个元素开始,该元素可以认为已经被排序
2.取出下一个元素,在已经排序的元素序列中从后向前扫描
3.如果该元素(已排序)大于新元素,将该元素移到下一位置
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5.将新元素插入到该位置后
4、快速排序
思路
1、选取第一个数为基准
2、将比基准小的数交换到前面,比基准大的数交换到后面
3、对左右区间重复第二步,直到各区间只有一个数
- 我们从数组中选择一个元素,我们把这个元素称之为中轴元素吧,然后把数组中所有小于中轴元素的元素放在其左边,所有大于或等于中轴元素的元素放在其右边,显然,此时中轴元素所处的位置的是有序的。也就是说,我们无需再移动中轴元素的位置。
- 从中轴元素那里开始把大的数组切割成两个小的数组(两个数组都不包含中轴元素),接着我们通过递归的方式,让中轴元素左边的数组和右边的数组也重复同样的操作,直到数组的大小为1,此时每个元素都处于有序的位置。
5、希尔排序
希尔排序可以说是插入排序的一种变种。无论是插入排序还是冒泡排序,如果数组的最大值刚好是在第一位,要将它挪到正确的位置就需要 n - 1 次移动。也就是说,原数组的一个元素如果距离它正确的位置很远的话,则需要与相邻元素交换很多次才能到达正确的位置,这样是相对比较花时间了。
-
希尔排序就是为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序。
-
希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是 h = n / 2,接着让 h = n / 4,让 h 一直缩小,当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了。
6、归并排序
将一个大的无序数组有序,我们可以把大的数组分成两个,然后对这两个数组分别进行排序,之后在把这两个数组合并成一个有序的数组。由于两个小的数组都是有序的,所以在合并的时候是很快的。 通过递归的方式将大的数组一直分割,直到数组的大小为 1,此时只有一个元素,那么该数组就是有序的了,之后再把两个数组大小为1的合并成一个大小为2的,再把两个大小为2的合并成4的 … 直到全部小的数组合并起来。
算法思想
1、把长度为n的输入序列分成两个长度为n/2的子序列;
2、对这两个子序列分别采用归并排序;
3、 将两个排序好的子序列合并成一个最终的排序序列。
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
7 堆排序
首先堆heap是一种数据结构,是一棵完全二叉树且满足性质:所有非叶子结点的值均不大于或均不小于其左、右孩子结点的值.
堆排序思想
-
堆排序的基本思想是利用heap这种数据结构(可看成一个完全二叉树),使在排序中比较的次数明显减少。
-
堆排序的时间复杂度为O(n*log(n)), 非稳定排序,原地排序(空间复杂度O(1))。
-
堆排序的关键在于建堆和调整堆,下面简单介绍一下建堆的过程:
第1趟将索引0至n-1处的全部数据建大顶(或小顶)堆,就可以选出这组数据的最大值(或最小值)。将该堆的根节点与这组数据的最后一个节点交换,就使的这组数据中最大(最小)值排在了最后。
第2趟将索引0至n-2处的全部数据建大顶(或小顶)堆,就可以选出这组数据的最大值(或最小值)。将该堆的根节点与这组数据的倒数第二个节点交换,就使的这组数据中最大(最小)值排在了倒数第2位。
…
第k趟将索引0至n-k处的全部数据建大顶(或小顶)堆,就可以选出这组数据的最大值(或最小值)。将该堆的根节点与这组数据的倒数第k个节点交换,就使的这组数据中最大(最小)值排在了倒数第k位。
- 其实整个堆排序过程中, 我们只需重复做两件事:
建堆(初始化+调整堆, 时间复杂度为O(n));
拿堆的根节点和最后一个节点交换(siftdown, 时间复杂度为O(n*log n) ).
- 因而堆排序整体的时间复杂度为O(n*log n).
8 计数排序(Count Sort)
是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。
计数排序的思想是在给定的一组序列中,先找出该序列中的最大值和最小值,从而确定需要开辟多大的辅助空间,每一个数在对应的辅助空间中都有唯一的下标。
找出序列中最大值和最小值,开辟Max-Min+1的辅助空间
最小的数对应下标为0的位置,遇到一个数就给对应下标处的值+1,。
遍历一遍辅助空间,就可以得到有序的一组序列
9 .桶排序
基本思想
桶排序的基本思想是假设数据在[min,max]之间均匀分布,其中min、max分别指数据中的最小值和最大值。那么将区间[min,max]等分成n份,这n个区间便称为n个桶。将数据加入对应的桶中,然后每个桶内单独排序。由于桶之间有大小关系,因此可以从大到小(或从小到大)将桶中元素放入到数组中。
10.基数排序
算法思想
基数排序是一种非比较型整数排序算法,其原理是将众多数字按位分隔后进行排序。
实现步骤:
1.将所有待比较的数字(正整数)统一为同一长度,位数不够的数字前面补0;
2.按照从个位,十位,百位······从低到高的顺序进行排序
3.完成从低位到高位的排序后,待排序数字也就完成了排序
#include <iostream>
#include <vector>
#include <algorithm>
#include <list>
using namespace std;
vector<int> bubbleSort(vector<int>& nums) {
int size = nums.size();
bool flag = false;
for (int i = 0; i < size; ++i) {
flag = false;
for (int j = 0; j < size - i; ++j) {
if (nums[j] > nums[j + 1]) {
swap(nums[j], nums[j + 1]);
flag = true;
}
}
if (!flag) break;
}
return nums;
}
vector<int> selectSort(vector<int>& nums) {
int size = nums.size();
int index = 0;
for (int i = 0; i < size; ++i) {
index = i;
for (int j = i + 1; j < size; ++j) {
if (nums[j] < nums[index]) {
index = j;
}
}
swap(nums[i], nums[index]);
}
return nums;
}
vector<int> insertSort(vector<int>& nums) {
int size = nums.size();
for (int i = 0; i < size; ++i) {
int temp = nums[i];
int j = i;
while (j > 0 && nums[j - 1] > temp) {
nums[j] = nums[j - 1];
j--;
}
nums[j] = temp;
}
return nums;
}
//quickSort with midValue
int midValue(vector<int>& nums, int left, int right) {
int mid = left + (right - left) / 2;
if (nums[left] < nums[right]) {
if (nums[mid] > nums[right]) return right;
else if (nums[mid] < nums[left]) return left;
else return mid;
}
else {
if (nums[mid] > nums[left]) return left;
else if (nums[right] > nums[mid]) return right;
else return mid;
}
}
vector<int> quickSort(vector<int>& nums, int low, int high) {
if (low >= high) return nums; //递归一定要有结束标志
int mid = midValue(nums, low, high);
swap(nums[low], nums[mid]);
int first = low, last = high;
int key = nums[first];
while (first < last) {
while (first < last && nums[last] >= key) last--;
if (first < last) nums[first++] = nums[last];
while (first < last && nums[first] <= key) first++;
if (first < last) nums[last--] = nums[first];
}
nums[first] = key;
quickSort(nums, low, first - 1);
quickSort(nums, first + 1, high);
return nums;
}
vector<int> adjust(vector<int>& nums, int start, int end) {
int dad = start;
int son = dad* 2 + 1;
while (son <= end) {
if (son + 1 <= end && nums[son] < nums[son + 1])
son++;
if (nums[dad] >= nums[son]) break;
else {
swap(nums[dad], nums[son]);
dad = son;
son = dad* 2 + 1;
}
}
return nums;
}
vector<int> heapSort(vector<int>& nums, int size) {
for (int i = size / 2 - 1; i >= 0; --i) {
adjust(nums, i, size);
}
for (int i = size - 1; i >= 0; --i) {
swap(nums[i], nums[0]);
adjust(nums, 0, i - 1);
}
return nums;
}
vector<int> mergeSort(vector<int>& nums, vector<int>& temp, int start, int end) {
if (start >= end) return temp;
int low1 = start, high2 = end, mid = start + (end - start) / 2;
int high1 = mid, low2 = mid + 1;
mergeSort( temp, nums,low1, high1);
mergeSort( temp, nums,low2, high2);
int index = start; // 缓存数组下标
while (low1 <= high1 && low2 <= high2) {
temp[index++] = nums[low1] <= nums[low2] ? nums[low1++] : nums[low2++];
}
while (low1 <= high1) temp[index++] = nums[low1++];
while (low2 <= high2) temp[index++] = nums[low2++];
// for (index = start; index <= end; ++index) {
// nums[index] = temp[index];
// }
// for (auto a : nums)
// cout << a << " ";
// cout << endl;
return temp;
}
vector<int> shellSort(vector<int>& nums) {
int size = nums.size();
for (int gap = size / 2; gap > 0; gap /= 2) { // 注意gap > 0
for (int i = gap; i < size; ++i) {
int temp = nums[i];
int j = i - gap;
for (; j >= 0 && temp <= nums[j]; j -= gap) { // 注意j >= 0
nums[j + gap] = nums[j];
}
nums[j + gap] = temp;
}
}
return nums;
}
vector<int> countSort(vector<int>& nums, int maximal) { //计数排序要求元素能够作为数组的下标,自然不能是负数
int len = nums.size();
if (len < 1) return nums;
vector<int> count(maximal + 1, 0);
vector<int> temp(nums);
for (auto a : nums) {
count[a]++;
}
for (int i = 1; i <= maximal; ++i) {
count[i] += count[i - 1];
}
for (int i = len - 1; i >= 0; --i) {
nums[count[temp[i]] - 1] = temp[i];
count[temp[i]]--;
}
return nums;
}
vector<int> bucketSort(vector<int>& nums, int len) {
// find the maximal value of array
int max = nums[0];
int min = max;
for (int i = 0; i < len; ++i) {
if (nums[i] > max) max = nums[i];
}
for (int i = 0; i < len; ++i) {
if (nums[i] < min) min = nums[i];
}
// get the count of bucket
int bucketCounts = (max - min) / len + 1;
vector<vector<int>> bucketArray(bucketCounts);
// assign each value to bucket arrays.
for (int i = 0; i < len; ++i) {
int num = (nums[i] - min) / len;
bucketArray[num].push_back(nums[i]);
}
// sort each bucket
for (int i = 0; i < bucketCounts; ++i) {
sort(bucketArray[i].begin(), bucketArray[i].end());
}
// collect value from radix arrays to data
int index = 0;
for (int i = 0; i < bucketCounts; ++i) {
for (int j = 0; j < bucketArray[i].size(); ++j) {
nums[index++] = bucketArray[i][j];
}
}
return nums;
}
int my_max(vector<int>& nums, int n){
int temp = 1;
int d = 10;
for (int i = 0; i < n; i++){
if (nums[i]>d){
d *= 10;
temp++;
}
}
return temp;
}
vector<int> radixSort(vector<int>& nums, int n){
int max = my_max(nums, n);
list<int>my_list[10];
//最高位决定循环的次数!
for (int i = 0,d=1; i < max; i++,d*=10){
for (int j = 0; j < n; j++){
//核心算法以及注意是按照顺序从前往后插入的!
my_list[(nums[j] / d) % 10].push_back(nums[j]);
}
for (int k = 0,m=0; k < 10; k++){
while (!my_list[k].empty()){
//这里我们取出来的时候也是从前往后取出来,这样才是排好序的,同时不要忘记把桶清空呀!
nums[m++] = my_list[k].front();
my_list[k].pop_front();
}
}
}
return nums;
}
int main()
{
vector<int> nums{2,4,3,6,5,9,7,1, 11, 16, 13};
vector<int> temp(nums);
int maximal = nums[0];
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] > maximal) maximal = nums[i];
}
int len = nums.size();
vector<int> res;
//res = bubbleSort(nums);
//res = selectSort(nums);
//res = insertSort(nums);
//res = quickSort(nums, 0, nums.size() - 1);
//res = heapSort(nums,nums.size());
//res = mergeSort(nums, temp, 0, nums.size() - 1);
//res = shellSort(nums);
//res = countSort(nums, maximal);
//res = bucketSort(nums, len);
res = radixSort(nums, len);
for (auto a : res)
cout << a << " ";
}
注意:十个排序算法都已经测试过,其中需要注意的是由于计数排序需要元素作为下标,所有我给测试数据时候,都用的是正数。
优化:比如冒泡排序,我们可以采用flag判断如果已经有序,我们可以直接中断循环,节省时间;
比如快速排序,我们采用三个数每次都取最中间的数,这样的目的是让快排结果不会有最坏结果。