概述
排序有内部排序和外部排序,内部排序是记录在内存中进行排序,而外部排序是因排序的数据容纳全部的排序记录,在排序过程中需要访问外存。
八大排序时间和空间复杂度比较:
当n较大,则采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或者归并排序。
快速排序: 是目前基于比较的内部派苏中被认为是最好的方法,当代排序的关键字是随机分布时,快速排序的平均时间最短。
1. 插入排序----直接插入排序(Straight Insertion Sort)
直接插入排序的核心思想:将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过。
因此,从上面的描述中我们可以发现,直接插入排序可以用两个循环完成:
- 第一层循环:遍历待比较的所有数组元素
- 第二层循环:将本轮选择的元素(selected)与已经排好序的元素(ordered)相比较。
如果:selected > ordered,那么将二者交换
代码实现:
// 直接插入排序
void InsertSort(int a[], int n)
{
for (int i = 1; i < n; i++)
{
// 若第i个元素大于i - 1元素,直接插入
if (a[i] < a[i-1])
{
int j = i - 1;
int x = a[i]; // 记录为待排序元素
a[i] = a[i - 1]; // 先向后移动一个元素
// 查找在有序表中的插入位置
while (x < a[j])
{
a[j + 1] = a[j];
j--;
a[j + 1] = x;
}
}
}
}
2. 插入排序----希尔排序
希尔排序的算法思想:将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。
同样的:从上面的描述中我们可以发现:
希尔排序的总体实现应该由三个循环完成:
1> 第一层循环:将gap依次折半,对序列进行分组,直到gap=1
2> 第二、三层循环:也即直接插入排序所需要的两次循环。具体描述见上
代码实现:
// 设置排序增量 n表示排序的个数
void sellSort(int a[], int n)
{
int dk = n / 2;
while (dk >= 1)
{
shellInsertSort(a, n, dk);
dk = dk / 2;
}
}
// 按照步长dk进行排序
void ShellInsertSort(int a[], int n, int dk)
{
for(int i= dk; i<n; ++i)
{
//若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
if(a[i] < a[i-dk])
{
int j = i-dk;
int x = a[i]; //复制为哨兵,即存储待排序元素
a[i] = a[i-dk]; //首先后移一个元素
while(x < a[j])
{
//查找在有序表的插入位置
a[j+dk] = a[j];
j -= dk; //元素后移
}
a[j+dk] = x; //插入到正确位置
}
}
}
3. 选择排序----简单选择排序
算法思想
简单选择排序的基本思想:比较+交换。
从待排序序列中,找到关键字最小的元素;
如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
从余下的 N - 1 个元素中,找出关键字最小的元素,重复(1)、(2)步,直到排序结束。
因此我们可以发现,简单选择排序也是通过两层循环实现。
第一层循环:依次遍历序列当中的每一个元素
第二层循环:将遍历得到的当前元素依次与余下的元素进行比较,符合最小元素的条件,则交换。
代码实现:
// 选择最小的元素
void selectMin(int a[], int n, int i)
{
int k = i;
for (int j = i+1; j < n; ++j)
{
if(a[k] > a[j])
k = j;
}
return k;
}
// 选择排序
void selectSort(int a[], int n)
{
int key, tmp;
for (int i = 0; i < n; ++i)
{
key = selectMin(a, n, i);
if (key != i)
{
tmp = a[i];
a[i] = a[key];
a[key] = tmp;
}
}
}
4. 选择排序----堆排序
算法思想
堆排序可以按照以下步骤来完成:
首先将序列构建称为大顶堆;
(这样满足了大顶堆那条性质:位于根节点的元素一定是当前序列的最大值)
取出当前大顶堆的根节点,将其与序列末尾元素进行交换;
(此时:序列末尾的元素为已排序的最大值;由于交换了元素,当前位于根节点的堆并不一定满足大顶堆的性质)
对交换后的n-1个序列元素进行调整,使其满足大顶堆的性质;
重复上述步骤,直至堆中只有1个元素为止
代码实现:
// 堆排序实现
5.交换排序----冒泡排序(Bubble Sort)
基本思想
冒泡排序思路比较简单:
1> 将序列当中的左右元素,依次比较,保证右边的元素始终大于左边的元素;( 第一轮结束后,序列最后一个元素一定是当前序列的最大值;)
2> 对序列当中剩下的n-1个元素再次执行步骤1。
3> 对于长度为n的序列,一共需要执行n-1轮比较。(利用while循环可以减少执行次数)
代码实现:
// 冒泡实现
void bubbleSort(int a[], int n){
for(int i =0 ; i< n-1; ++i) {
for(int j = 0; j < n-i-1; ++j) {
if(a[j] > a[j+1])
{
int tmp = a[j] ;
a[j] = a[j+1] ;
a[j+1] = tmp;
}
}
}
}
6. 交换排序----快速排序
算法思想
快速排序的基本思想:挖坑填数+分治法
a. 从序列当中选择一个基准数(pivot)
b. 在这里我们选择序列当中第一个数最为基准数
c. 将序列当中的所有数依次遍历,比基准数大的位于其右侧,比基准数小的位于其左侧
重复步骤b,直到所有子集当中只有一个元素为止。
用伪代码描述如下:
1> i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2> j–由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3> i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4> 再重复执行2,3二步,直到i==j,将基准数填入a[i]中
代码实现:
// 快速排序
void print(int a[], int n){
for(int j= 0; j<n; j++){
cout<<a[j] <<" ";
}
cout<<endl;
}
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int partition(int a[], int low, int high)
{
int privotKey = a[low]; //基准元素
while(low < high){ //从表的两端交替地向中间扫描
while(low < high && a[high] >= privotKey) --high; //从high 所指位置向前搜索,至多到low+1 位置。将比基准元素小的交换到低端
swap(&a[low], &a[high]);
while(low < high && a[low] <= privotKey ) ++low;
swap(&a[low], &a[high]);
}
print(a,10);
return low;
}
void qsort_improve(int r[ ],int low,int high, int k){
if( high -low > k ) { //长度大于k时递归, k为指定的数
int pivot = partition(r, low, high); // 调用的Partition算法保持不变
qsort_improve(r, low, pivot - 1,k);
qsort_improve(r, pivot + 1, high,k);
}
}
void quickSort(int r[], int n, int k){
qsort_improve(r,0,n,k);//先调用改进算法Qsort使之基本有序
//再用插入排序对基本有序序列排序
for(int i=1; i<=n;i ++){
int tmp = r[i];
int j=i-1;
while(tmp < r[j]){
r[j+1]=r[j]; j=j-1;
}
r[j+1] = tmp;
}
}
int main(){
int a[10] = {3,1,5,7,2,4,9,6,10,8};
cout<<"初始值:";
print(a,10);
quickSort(a,9,4);
cout<<"结果:";
print(a,10);
}
7. 归并排序
基本思想:
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个典型的应用。它的基本操作是:将已有的子序列合并,达到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
归并排序其实要做两件事:
分解----将序列每次折半拆分
合并----将划分后的序列段两两排序合并
因此,归并排序实际上就是两个操作,拆分+合并
那么,如何合并?
假设L[first…mid]为第一段,L[mid+1…last]为第二段,并且两端已经有序,现在我们要将两端合成达到L[first…last]并且也有序。
首先依次从第一段与第二段中取出元素比较,将较小的元素赋值给temp[];
重复执行上一步,当某一段赋值结束,则将另一段剩下的元素赋值给temp[];
此时将temp[]中的元素复制给L[],则得到的L[first…last]有序;
如何分解?
在这里,我们采用递归的方法,首先将待排序列分成A,B两组;然后重复对A、B序列
分组;直到分组后组内只有一个元素,此时我们认为组内所有元素有序,则分组结束。
代码实现:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
/*该函数将数组下标范围[l1,r1]和[l2,r2]的有序序列合并成一个有序序列*/
void merge(vector<int>& nums, int l1, int r1, int l2, int r2 ) {
int i = l1; //左半部分起始位置
int j = l2; //右半部分起始位置
int n = (r1 - l1 + 1) + (r2 - l2 + 1); //要合并的元素个数
vector<int> temp(n); //辅助数组
int k = 0; //辅助数组其起始位置
while (i <= r1&&j <= r2) { //挑选两部分中最小的元素放入辅助数组中
if (nums[i] < nums[j])
temp[k++] = nums[i++];
else
temp[k++] = nums[j++];
}
//如果还有剩余,直接放入到辅助数组中
while (i <= r1)
temp[k++] = nums[i++];
while (j <= r2)
temp[k++] = nums[j++];
//更新原始数组元素
for (int i = 0; i < n;i++)
{
nums[l1 + i] = temp[i];
}
}
/*二路归并排序(递归实现)*/
void MergeSort(vector<int>& nums,int start, int end) {
if (start < end) {
int mid = (start + end) >> 1; //分割序列
MergeSort(nums, start, mid); //对序列左半部分进行规并排序
MergeSort(nums, mid + 1, end); //对序列右半部分进行规并排序
merge(nums, start, mid, mid + 1, end); //合并已经有序的两个序列
}
}
/*二路归并排序(迭代实现)*/
void MergeSort1(vector<int>& nums, int start, int end)
{
int n = nums.size();
if (start < end) {
//step为组内元素个数,step/2为左子区间元素个数
for (int step = 2; step/2 <n; step *= 2) {
//每step个元素一组,组内前step/2和后step/2个元素进行合并
for (int i = 0; i < n; i += step) {
int mid = i + step / 2 - 1; //左子区间元素个数为step/2
if(mid+1<n) //右子区间存在元素个数则合并
//左子区间为[i,mid],右子区间为[mid+1, min(i+step-1, n-1)]
merge(nums, i, mid, mid + 1, min(i + step - 1, n-1));
}
}
}
}
int main() {
vector<int> nums{ 1,4,3,2,5,6,3 };
MergeSort(nums,0,6);
// MergeSort1(nums, 0, 6);
for (auto x : nums)
cout << x << " ";
cout << endl;
return 0;
}
8. 基数排序
基本思想:
基数排序:是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
说基数排序之前,我们简单介绍桶排序:
算法思想:是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。
例如要对大小为[1…1000]范围内的n个整数A[1…n]排序
首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1…10]的整数,集合B[2]存储 (10…20]的整数,……集合B[i]存储( (i-1)10, i10]的整数,i = 1,2,…100。总共有 100个桶。
然后,对A[1…n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。
最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果
对每个桶中的数字采用快速排序,那么整个算法的复杂度是
O(n + m * n/m*log(n/m)) = O(n + nlogn – nlogm)
从上式看出,当m接近n的时候,桶排序复杂度接近O(n)
当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的 ,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。
前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:
1> 空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间;
2> 待排序的元素都要在一定的范围内等等。
代码实现:
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
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 = newint[n];
int *count = newint[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;
}