排序分类
首先分为内部排序和外部排序,本文主要研究内部排序。
外部排序排序过程需要访问外存,内存外存也要结合使用,适合处理数据量很大的数据,
如1000T等,使用多路归并排序算法和败者树。
内部排序按照原理分为5大类。插入,选择,交换,归并,基数排序。
插入排序:直接插入排序,折半插入排序,希尔排序。
选择排序:简单选择排序,堆排序。
交换排序:冒泡排序,快速排序。
归并排序。
计数排序。
桶排序。
基数排序。
排序总结
时间复杂度:算法执行的时间
空间复杂度:算法执行占用的内存
排序方式:in-place(占用常数内存,不占用额外内存),out-place(占用额外内存)
稳定性:原来相等的两个数排序后不乱序为稳定
一、冒泡排序
冒泡排序就是把⼩的元素往前调或者把⼤的元素往后调,比较是相邻的两个元素比较,交换也发⽣在这两个元素之间。
法一
//时间复杂度:O(n2)
//空间复杂度:O(1)
// 稳定
using std::cout;
using std::endl;
using std::vector;
using std::swap;
void print(vector<int>& nums) {
for (auto i : nums) {
cout << i << " ";
}
cout << endl;
}
void bubbleSort(vector<int>& nums) {
int n = nums.size();
bool flag = false;
for (int i = 0; i < n - 1; ++i) {//i = 0 起,循环了n - 1趟,更符合规范理解
//for (int i = 0; i < n; ++i) {//i = 0 起,循环了n 趟,不影响结果
flag = false;
for (int j = 0; j < n - 1 - i; ++j) {
if (nums[j] > nums[j + 1]) {
//某⼀趟排序中,只要发⽣⼀次元素交换,flag就从false变为了true
//也即表示这⼀趟排序还不能确定所剩待排序列是否已经有序,应继续下⼀趟循环
swap(nums[j], nums[j + 1]);
flag = true;
}
}
//但若某⼀趟中⼀次元素交换都没有,即依然为flag = false
//那么表明所剩待排序列已经有序
//不必再进⾏趟数⽐,外层循环应该结束,即此时if (!flag) break; 跳出循环
if (!flag) { break; }
}
}
int main() {
vector<int> nums = { 8,9,1,4,2,3,6,7,5,5 };
bubbleSort(nums);
print(nums);
return 0;
}
法二
#include <iostream>
#include <vector>
using namespace std;
//时间复杂度:O(n2)
//空间复杂度:O(1)
// 稳定
void bubble_Sort(vector<int>& nums)
{
int n = nums.size();
bool valid = true;
for (int i = 0; i < n&&valid ; i++)
{
valid=false;
for (int j = n - 2; j >= i; j--)//气泡一样往前冒
{
if (nums[j] > nums[j + 1])
{
swap(nums[j], nums[j + 1]);
valid=true;//交换成功为true
// int temp = nums[j];
// nums[j] = nums[j + 1];
// nums[j + 1] = temp;
}
}
}
}
int main()
{
vector<int>v{ 1,10,24,12,16,8,18,89,25 };
bubble_Sort(v);
cout<<"排序从小到大:" << endl;
for(vector<int>::iterator it=v.begin(); it!=v.end();it++)
{
cout << *it << " ";
}
cout << endl;
system("pause");
return 0;
}
二、插入排序:类似打扑克牌
插⼊排序是在⼀个已经有序的⼩序列的基础上,⼀次插⼊⼀个元素。
//直接插入排序
//稳定
//原始数据越有序,排序时间越快
//时间复杂度最好O(n):本来有序
//时间复杂度最坏O(n2):逆序
//空间复杂度O(1):一个哨兵额外空间
// 优化地方
//1)比较次数
//2)移动次数
#include <iostream>
#include <vector>
using namespace std;
//直接插入:顺序查找插入位置
//void Insert_Direct(vector<int>&s)
//{
// int n = s.size();
// for (int i = 1; i < n; i++)
// {
// if (s[i] < s[i - 1])//如果后一个比前一个大,就不用移动了
// {
// int j=0;
// int temp = s[i];//哨兵
// for (j = i - 1; j>=0&&s[j] > temp; j--)
// {
// s[j + 1] = s[j];
// }
// s[j + 1] = temp;
// }
// }
//}
//折半插入排序
//时间和空间复杂度不变
//n较大时,减小比较次数查找快,移动次数没有减小
//二分查找插入位置
void Insert_Binary(vector<int>& s)
{
int n = s.size();
for (int i = 1; i < n; ++i)
{
int temp = s[i];//哨兵
int low = 0, high = i - 1;
while (low <= high)
{
int mid = low + (high - low) / 2;
if (temp > s[mid])
{
low = mid + 1;
}
else
{
high = mid - 1;
}
}
for (int j = i - 1; j >= high + 1; --j)
{
s[j + 1] = s[j];
}
s[high+ 1] = temp;
}
}
int main()
{
vector<int>v{ 1,10,24,12,16,8 };
//Insert_Direct(v);
Insert_Binary(v);
cout<<"排序从小到大:" << endl;
for(vector<int>::iterator it=v.begin(); it!=v.end();it++)
{
cout << *it << " ";
}
cout << endl;
system("pause");
return 0;
}
三、选择排序
选择排序是给每个位置选择当前元素最⼩的,⽐如给第⼀个位置选择最⼩的,在剩余元素⾥⾯给>⼆个元素选择第⼆⼩的,依次类推,直到第n-1个元素,第n个 元素不⽤选择了,因为只剩下它⼀个最⼤的元素了。
//简单选择排序
//时间复杂度:O(n2)
//空间复杂度:O(1)
//不稳定
//待排序数据元素中选出最小(最大)元素,直接放在起始位置
//剩余未排序的元素中找到最小(最大)元素,放在已排序序列的末尾
#include <iostream>
#include <vector>
using namespace std;
void choose_Sort(vector<int>& nums)
{
int n = nums.size();
//外层循环完成数据交换,内层循环完成数据比较
for (int i = 0; i < n; i++)
{
int min = i;
for (int j = i + 1; j < n; j++)
{
if (nums[j] < nums[min])
{
min = j;
}
}
if (min != i)
{
swap(nums[min], nums[i]);
}
}
}
int main()
{
vector<int>v{ 1,10,24,12,16,8,18,89,25 };
choose_Sort(v);
cout<<"排序从小到大:" << endl;
for(vector<int>::iterator it=v.begin(); it!=v.end();it++)
{
cout << *it << " ";
}
cout << endl;
system("pause");
return 0;
}
四、希尔排序
希尔排序可以说是插⼊排序的⼀种变种。⽆论是插⼊排序还是冒泡排序,如果数组的最⼤值刚好是在第⼀位,要将它挪到正确的位置就需要 n - 1 次移动。也就是说,原数组的⼀个元素如果距离它正确的位置很远的话,则需要与相邻元素交换很多次才能到达正确的位置,这样是相对比较花时间了。
希尔排序就是为了加快速度简单地改进了插⼊排序,交换不相邻的元素以对数组的局部进⾏排序。
希尔排序的思想是采⽤插⼊排序的⽅法,先让数组中任意间隔为 h 的元素有序,刚开始 h 的⼤⼩可以是 h = n / 2,
接着让 h = n / 4,让 h ⼀直缩⼩,当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了
//希尔排序
//时间复杂度O(n^1.25)~O(1.6n^1.25)
//空间复杂度O(1)
//跳跃式接近,慢慢的有序,最后一次少量移动
//增量序列是递减的,互质的,最后一个增量值是1
#include <iostream>
#include <vector>
using namespace std;
void shell_Sort(vector<int>& s)
{
int n = s.size();
int dk = n;
do
{
dk = dk / 3 + 1;//dk步长
for (int i = dk; i < n; i += dk)
{
if (s[i] < s[i - dk])
{
int temp = s[i];
int j = 0;
for (j = i - dk; j >= 0 && s[j] > temp; j -= dk)
{
s[j + dk] = s[j];
}
s[j + dk] = temp;
}
}
} while (dk > 1);//dk=1跳出
}
int main()
{
vector<int>v{ 1,10,24,12,16,8,18,89,25 };
shell_Sort(v);
cout<<"排序从小到大:" << endl;
for(vector<int>::iterator it=v.begin(); it!=v.end();it++)
{
cout << *it << " ";
}
cout << endl;
system("pause");
return 0;
}
五、快速排序
1、选取第⼀个数为基准
2、将⽐基准⼩的数交换到前⾯,⽐基准⼤的数交换到后⾯
3、对左右区间重复第⼆步,直到各区间只有⼀个数
我们从数组中选择⼀个元素,我们把这个元素称之为中轴元素吧,然后把数组中所有⼩于中轴元素的元素放在其左边,所有⼤于或等于中轴元素的元素放在其右边,显然,此时中轴元素所处的位置的是有序的。也就是说,我们⽆需再移动中轴元素的位置。
从中轴元素那⾥开始把⼤的数组切割成两个⼩的数组(两个数组都不包含中轴元素),接着我们通过递归的⽅式,让中轴元素左边的数组和右边的数组也重复同样的操作,直到数组的⼤⼩为1,此时每个元素都处于有序的位置。
//快速排序算法
// 不稳定
//原始数据越无序,排序时间越快
//时间复杂度:O(nlog(n))
//空间复杂度:O(n)
#include <iostream>
using namespace std;
#include <vector>
//递归快速排序,从小到大
//void Quick_Sort(vector<int>& s,int low,int high)
//{
// if (low >= high) return;
// int i, j, base, temp;
// i = low, j = high;
// base = s[low];//取最左边的数为基准数
// while(i<j)
// {
// while (s[j] >= base && i < j) j--;
// while (s[i] <= base && i < j) i++;
// if (i < j)
// {
// temp = s[i];
// s[i] = s[j];
// s[j]= temp;
// }
// }
// //基准数归位
// s[low] = s[i];
// s[i] = base;
// Quick_Sort(s, low, i - 1);//递归左子表
// Quick_Sort(s, i+ 1, high);//递归右子表
//}
递归快速排序,从大到小
//void Quick_Sort(vector<int>& s, int low, int high)
//{
// if (low >= high) return;
// int i, j, base, temp;
// i = low, j = high;
// base = s[low];//取最左边的数为基准数
// while (i < j)
// {
// while (s[j] <= base && i < j) j--;
// while (s[i] >= base && i < j) i++;
// if (i < j)
// {
// temp = s[i];
// s[i] = s[j];
// s[j] = temp;
// }
// }
// //基准数归位
// s[low] = s[i];
// s[i] = base;
// Quick_Sort(s, low, i - 1);//递归左子表
// Quick_Sort(s, i + 1, high);//递归右子表
//}
int Is_Pivot(vector<int>& nums, int low, int high);
void Quick_sort(vector<int>& nums, int low, int high)
{
while (low < high)
{
int pivot = Is_Pivot(nums, low, high);
Quick_sort(nums, low, pivot - 1);
low = pivot + 1;
}
}
int Is_Pivot(vector<int>& nums, int low, int high)
{
int temp = nums[low];//哨兵
while (low < high)
{
while (nums[high] >= temp && low < high) high--;
nums[low] = nums[high];
while (nums[low] <= temp && low < high) low++;
nums[high] = nums[low];
}
nums[low] = temp;
return low;//返回枢轴点下标
}
int main()
{
vector<int>v{ 1,10,24,12,16,8 };
Quick_sort(v,0,5);
cout<<"排序从小到大:" << endl;
//cout << "排序从大到小:" << endl;
for(vector<int>::iterator it=v.begin(); it!=v.end();it++)
{
cout << *it << " ";
}
cout << endl;
system("pause");
return 0;
}
六、堆排序
//堆排序
//不稳定
//时间复杂度:O(nlog(n))
//空间复杂度:O(1)
#include <iostream>
#include <vector>
using namespace std;
void heapadjust(vector<int>& nums, int i, int maxsize);
int heapsort(vector<int>& nums, int k)
{
int n = nums.size();
for (int i = n / 2; i >= 1; i--)
{
heapadjust(nums, i, n);
}
int size = n;
for (int i = 1; i < k; i++)
{
swap(nums[0], nums[size - 1]);
heapadjust(nums, 1, size - 1);
size--;
}
return nums[0];
}
//堆的建立
void heapadjust(vector<int>& nums, int i, int maxsize)
{
int temp = nums[i - 1];
for (int j = i * 2; j <= maxsize; j *= 2)
{
if (j + 1 <= maxsize && nums[j] > nums[j - 1])
{
j++;
}
if (temp > nums[j - 1])break;
nums[i - 1] = nums[j - 1];
i = j;
}
nums[i - 1] = temp;
}
//调整大顶堆,升序输入
//n数组长度,i待调整节点,数组下标从0开始
void heapify(vector<int>& nums,int n,int i)
{
int largest = i, lson = 2 * i + 1, rson = 2 * i + 2;
if (lson < n && nums[largest] < nums[lson])
{
largest = lson;
}
if (rson < n && nums[largest] < nums[rson])
{
largest = rson;
}
if (largest != i)
{
swap(nums[largest], nums[i]);
heapify(nums, n, largest);//递归待调整节点largest
}
}
void heap_Sort(vector<int>& nums, int n)
{
//建堆
for (int i = n / 2 - 1; i >= 0; i--)
{
heapify(nums, n, i);//调整节点n/2-1
}
//排序
for (int i = n - 1; i >= 0; i--)
{
swap(nums[i], nums[0]);
heapify(nums, i, 0);//调整堆顶节点
}
}
int main()
{
vector<int>v{ 1,10,24,12,16,8 };
heap_Sort(v, 6);
for(vector<int>::iterator it=v.begin(); it!=v.end();it++)
{
cout << *it << " ";
}
system("pause");
return 0;
}
七、归并排序
将⼀个⼤的⽆序数组有序,我们可以把⼤的数组分成两个,然后对这两个数组分别进⾏排序,之后在把这两个数组合并成⼀个有序的数组。由于两个⼩的数组都是有序的,所以在合并的时候是很快的。
通过递归的⽅式将⼤的数组⼀直分割,直到数组的⼤⼩为 1,此时只有⼀个元素,那么该数组就是有序的了,之后再把两个数组⼤⼩为1的合并成⼀个⼤⼩为2的,再把两个⼤⼩为2的合并成4的 … 直到全部⼩的数组合并起来。
归并排序是建⽴在归并操作上的⼀种有效的排序算法。该算法是采⽤分治法(Divide and Conquer)的⼀个⾮常典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;即先使每个⼦序列有序,再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为2-路归并
算法思想
1、把⻓度为n的输⼊序列分成两个⻓度为n/2的⼦序列;
2、对这两个⼦序列分别采⽤归并排序;
3、 将两个排序好的⼦序列合并成⼀个最终的排序序列。
void mergeSortCore(vector<int>& data, vector<int>& dataTemp, int low, int high) {
if (low >= high) return;
int len = high - low, mid = low + len / 2;
int start1 = low, end1 = mid, start2 = mid + 1, end2 = high;
mergeSortCore(data, dataTemp, start1, end1);
mergeSortCore(data, dataTemp, start2, end2);
int index = low;
while (start1 <= end1 && start2 <= end2) {
dataTemp[index++] = data[start1] < data[start2] ? data[start1++] : data[start2++];
}
while (start1 <= end1) {
dataTemp[index++] = data[start1++];
}
while (start2 <= end2) {
dataTemp[index++] = data[start2++];
}
for (index = low; index <= high; ++index) {
data[index] = dataTemp[index];
}
}
void mergeSort(vector<int>& data) {
int len = data.size();
vector<int> dataTemp(len, 0);
mergeSortCore(data, dataTemp, 0, len - 1);
}
八、计数排序
计数排序统计⼩于等于该元素值的元素的个数i,于是该元素就放在⽬标数组的索引i位(i≥0)。计数排序基于⼀个假设,待排序数列的所有数均为整数,且出现在(0,k)的区间之内。如果 k(待排数组的最⼤值) 过⼤则会引起较⼤的空间复杂度,⼀般是⽤来排序 0 到 100 之间的数字的最好的算法,但是它不适合按字⺟顺序排序⼈名。计数排序不是比较排序,排序的速度快于任何比较排序算法
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 计数排序
void CountSort(vector<int>& vecRaw, vector<int>& vecObj)
{
// 确保待排序容器⾮空
if (vecRaw.size() == 0)
return;
// 使⽤ vecRaw 的最⼤值 + 1 作为计数容器 countVec 的⼤⼩
int vecCountLength = (*max_element(begin(vecRaw), end(vecRaw))) + 1;
vector<int> vecCount(vecCountLength, 0);
// 统计每个键值出现的次数
for (int i = 0; i < vecRaw.size(); i++)
vecCount[vecRaw[i]]++;
// 后⾯的键值出现的位置为前⾯所有键值出现的次数之和
for (int i = 1; i < vecCountLength; i++)
vecCount[i] += vecCount[i - 1];
// 将键值放到⽬标位置
for (int i = vecRaw.size(); i > 0; i--) // 此处逆序是为了保持相同键值的稳定性
vecObj[--vecCount[vecRaw[i - 1]]] = vecRaw[i - 1];
}
int main()
{
vector<int> vecRaw = { 0,5,7,9,6,3,4,5,2,8,6,9,2,1 };
vector<int> vecObj(vecRaw.size(), 0);
CountSort(vecRaw, vecObj);
for (int i = 0; i < vecObj.size(); ++i)
cout << vecObj[i] << " ";
cout << endl;
return 0;
}
九、桶排序
将值为i的元素放⼊i号桶,最后依次把桶⾥的元素倒出来。
算法思想:
- 设置⼀个定量的数组当作空桶⼦。
- 寻访序列,并且把项⽬⼀个⼀个放到对应的桶⼦去。
- 对每个不是空的桶⼦进⾏排序。
- 从不是空的桶⼦⾥把项⽬再放回原来的序列中
function bucketSort(arr, bucketSize) {
if (arr.length === 0) {
return arr;
}
var i;
var minValue = arr[0];
var maxValue = arr[0];
for (i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i]; // 输⼊数据的最⼩值
} else if (arr[i] > maxValue) {
maxValue = arr[i]; // 输⼊数据的最⼤值
}
}
// 桶的初始化
var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数ᰁ为5
bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
var buckets = new Array(bucketCount);
for (i = 0; i < buckets.length; i++) {
buckets[i] = [];
}
// 利⽤映射函数将数据分配到各个桶中
for (i = 0; i < arr.length; i++) {
buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
}
arr.length = 0;
for (i = 0; i < buckets.length; i++) {
insertionSort(buckets[i]); // 对每个桶进⾏排序,这⾥使⽤了插⼊排
序
for (var j = 0; j < buckets[i].length; j++) {
arr.push(buckets[i][j]);
}
}
return arr;
}
十、基数排序
⼀种多关键字的排序算法,可⽤桶排序实现。
算法思想:
- 取得数组中的最⼤数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进⾏计数排序(利⽤计数排序适⽤于⼩范围数的特点)
int maxbit(int data[], int n) //ᬀ助函数,求数据的最⼤位数
{
int maxData = data[0]; ///< 最⼤数
/// 先求出最⼤数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
for (int i = 1; i < n; ++i)
{
if (maxData < data[i])
maxData = data[i];
int d = 1;
int p = 10;
while (maxData >= p)
{
//p *= 10; // Maybe overflow
maxData /= 10;
++d;
}
return d;
/* int d = 1; //保存最⼤的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;*/
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = new int[n];
int *count = new int[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进⾏d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete []tmp;
delete []count;
}
总结
以上就是本人学习排序算法过程中的一些总结,部分注释部分是不同的写法,可自由切换,都经过测试,均可跑通!