排序算法的衡量指标
1.时间复杂度:算法运行时间随数据量变大的增长趋势
2.空间复杂度:算法执行过程中需要占用的额外存储空间
3.稳定性:时间复杂度和空间复杂度是否恒定
ps:时间复杂度与空间复杂度均用大O符号,即O(1)、O(n)等表示
排序算法的分类
1.从存储设备分:内排序 和外排序 (内排序是指在排序过程中,待排序的所有记录都被存储在内存中。外排序指由于排序的记录个数太多,不能同时放置在内存中,整个排序过程需要在内外存之间多次交换数据)
2.按对关键字的操作:基于关键字比较的排序 和分布排序
常见排序算法
插入排序
插入排序是指将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数+1的有序表
插入排序步骤:
1.将待排序序列中的第一个元素当做初始时已经排好序的有序表
2.取待排序序列中的下一个元素 (记为item)作为待插入元素,并用该元素比较有序表中的元素
3.若有序表中的某个元素大于item,则将该元素向后移动一位;若有序表中某一元素小于等于item,则将item插入到该元素后
4.重复步骤1->3,直到待排序序列中的所有元素都被插入到有序表中
插入排序的时间复杂度 :若待排序序列本身有序 ,则时间复杂度为O(n) ;若待排序序列为逆序 ,则时间复杂度为O(n^2)
插入排序代码实现如下(C++):
cpp
//插入排序代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//排序函数
void insertionSort(vector<int>& nums, int n){
int tmp;//暂存无序序列中的元素
int j;//在循环外定义,方便内层循环结束后保存位置信息
//外层循环,枚举无序序列的起始位置
for(int i=1;i<=n;i++){
tmp=nums[i+1];//取出无序序列中的第一个元素暂存到tmp
//内层循环,从有序序列末尾向前找插入位置,同时移动元素
for(j=i;j>=1;j--){
//若有序序列的元素比tmp大,就将该元素向后挪一位
if(nums[j]>tmp){
nums[j+1]=nums[j];
}
//找到有序序列中第一个小于等于tmp的元素,就停止查找
else{
break;
}
}
nums[j+1]=tmp;//将该元素插入到有序序列中的正确位置
}
}
int main(){
//数据输入
int n;
cin>>n;
vector<int> nums(n+1);
for(int i=1;i<=n;i++){
cin>>nums[i];
}
insertionSort(nums, n);
//数据输出处理
cout<<"排序结果如下:"<<endl;
for(int i=1;i<=n;i++){
cout<<nums[i]<<" ";
}
cout<<endl;
return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/
希尔排序
希尔排序 (也称Shell排序)是插入排序的优化版本,实现思路是通过分组插入排序降低待排序序列的无序度,最后对整体执行一次普通插入排序。
希尔排序步骤:
1.定义一个gap (初始 gap=n/2,n为序列中元素的个数),之后每次gap = gap /2,直到gap = 1为止
2.从头遍历待排序序列,对每个下标间隔gap的元素执行插入排序
3.重复步骤1和步骤2直到gap = 1
4.对分组插入排序后的序列进行一次插入排序
希尔排序的时间复杂度 :平均时间复杂度为O(N^1.3) ,若待排序序列为逆序 ,则时间复杂度为O(n^2)
希尔排序代码实现如下(C++):
cpp
//希尔排序代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void shellSort(vector<int> &nums){
int cnt = 0;//记录排序趟数
int n = nums.size()-1;
//对待排序序列进行分组,步长逐步折半(gap表示将待排序序列分为多少组)
for(int gap = n/2;gap >= 1;gap = gap/2){
cnt++;//趟数+1
//遍历每个分组,对每个分组进行插入排序
for(int i = gap+1; i<=n; i++){
int tmp=nums[i];//取出一个分组中的第一个元素,并保存该元素
int j;//提前定义,便于后续保存位置
//向前遍历同组元素,找到插入位置
for(j = i-gap;j>0;j-=gap){
if(nums[j] > tmp){
nums[j + gap] = nums[j];//将比tmp大的元素后移
}
//找到插入位置,退出循环(同一分组内该位置前的元素都比tmp小,该位置后的元素都比tmp大)
else{
break;
}
}
nums[j+gap]=tmp;//插入元素到正确位置
}
cout<<"第"<<cnt<<"趟排序的结果:"<<endl;
for(int i=1;i<=n;i++){
cout<<nums[i]<<" ";
}
cout<<endl;
}
}
int main(){
//数据输入
int n;
cin>>n;
vector<int> nums(n+1);
for(int i=1;i<=n;i++) {
cin>>nums[i];
}
shellSort(nums);
//数据输出
cout<<"排序结果如下:"<<endl;
for(int i=1;i<=n;i++){
cout<<nums[i]<<" ";
}
cout<<endl;
return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
第1趟排序的结果:
7 9 3 5 6 12 11 13 8 10
第2趟排序的结果:
3 5 6 9 7 10 8 12 11 13
第3趟排序的结果:
3 5 6 7 8 9 10 11 12 13
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/
选择排序
顾名思义,就是每次从待排序序列中取出一个最小值,然后放在有序表中,直到排序完成
选择排序的时间复杂度 :与待排序序列本身是否有序无关,时间复杂度恒为O(n^2)
选择排序代码实现如下(C++):
cpp
//选择排序代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void selectionSort(vector<int> &nums){
int n = nums.size()-1;
int min_index;//保存最小的数据的下标
//从待排序序列中找到最小的数据,放到序列的第一个位置
for(int i=1;i<n;i++) {
min_index=i;//最小的数据的下标
int tmp;//暂时保存数据
for(int j=i+1;j<=n;j++){
//若当前数据小于nums[min_index],则更新min_index为当前下标
if(nums[j]<nums[min_index]){
min_index=j;
}
}
//把这一趟遍历的数据放在待排序序列的第一个位置
tmp=nums[i];
nums[i]=nums[min_index];
nums[min_index]=tmp;
}
}
int main(){
//数据输入
int n;
cin>>n;
vector<int> nums(n+1);
for(int i=1;i<=n;i++) {
cin>>nums[i];
}
selectionSort(nums);
//数据输出
cout<<"排序结果如下:"<<endl;
for(int i=1;i<=n;i++){
cout<<nums[i]<<" ";
}
cout<<endl;
return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/
堆排序
堆排序是一种基于堆的排序算法,是选择排序的优化版本
堆 :一棵完全二叉树,分为小顶堆和大顶堆两种类型
小顶堆 :每个父节点的值都小于等于其左右子节点的值,堆顶(根节点)是整个堆的最小值
大顶堆 :每个父节点的值都大于等于其左右子节点的值,堆顶(根节点)是整个堆的最大值
堆化 :将一棵无序的完全二叉树调整为堆结构的过程(以大顶堆为例,待排序序列转换成完全二叉树后,从该树的最下一层的最右边节点开始,向它的父亲节点遍历找到第一个不是叶子节点的节点(记为x),若x大于其左右子树,则继续向前遍历;若x小于它的任一左右子树,则从子树中选择最大的一个与节点x进行交换,然后向前遍历)
堆排序步骤(升序排序):
1.将待排序序列视为一棵完全二叉树(类似于层次遍历),从最后一个非叶子节点开始从下往上对每个节点进行堆化操作,直到得到一个大顶堆
2.交换堆顶元素(最大值)和堆的最后一个元素,将最大值放到有序表末尾,然后对新的堆顶元素进行堆化操作,使其成为大顶堆
3.重复步骤1和步骤2,直到堆的大小缩小为1,排序完成
堆排序的时间复杂度 :与待排序序列本身是否有序无关,时间复杂度恒为O(n logn) ,堆化操作时间复杂度为O(log n)
堆排序代码实现如下(C++):
cpp
//堆排序代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
/*
调整以i为根节点的子树为大顶堆(每个父节点的值都大于等于其左右子节点的值的完全二叉树)
nums:待排序数组
n:堆的大小
i:当前节点的索引
*/
void heapify(vector<int> &nums,int n,int i){
int max_data = i;//初始化最大值为根节点
int left = 2*i+1;
int right = 2*i+2;
//如果左子节点大于根节点
if(left < n && nums[left] > nums[max_data]){
max_data = left;
}
//如果右子节点大于根节点
if(right < n && nums[right] > nums[max_data]){
max_data = right;
}
//如果最大值不是根节点,则交换并递归调整
if(max_data != i){
swap(nums[i], nums[max_data]);
heapify(nums, n, max_data);
}
}
/*
排序函数(升序)
nums:待排序数组
*/
void heapSort(vector<int> &nums){
int n = nums.size();
//构建大顶堆
for(int i = n / 2 - 1;i >= 0;i--){
heapify(nums,n,i);
}
//遍历大顶堆,将堆顶的最大值移动到末尾
for(int i = n-1;i>0;i--){
swap(nums[0],nums[i]);
heapify(nums,i,0);
}
}
int main(){
// 测试数据
//数据输入处理
int n;//数组大小
cin>>n;
vector<int> nums(n+1);
for(int i=1;i<=n;i++){
cin>>nums[i];
}
heapSort(nums);
cout<<"排序结果如下:"<<endl;
for(int i=1;i<=n;i++){
cout<<nums[i]<<" ";
}
cout<<endl;
return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/
冒泡排序
从待排序序列的第一个元素开始,依次比较相邻的两个元素,若前者大于 后者,交换两者位置 ;若前者小于等于 后者,两元素位置保持不变
代码实现方式:两层循环
冒泡排序的时间复杂度 :若待排序序列本身有序 ,则时间复杂度为O(n) ;若待排序序列为逆序 ,则时间复杂度为O(n^2)
冒泡排序代码实现如下(C++):
cpp
//冒泡排序代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void bubbleSort(vector<int> &nums){
int n = nums.size()-1;
int tmp;//临时保存数据
bool flag=0;//剪枝操作(若序列本身有序,则flag==false,提前退出循环)
for(int i=1;i<=n-1;i++){
flag=0;//对每个元素进行比较前先恢复flag为原状态
for(int j=1;j<=n-i;j++){
if(nums[j]>nums[j+1]){
flag=true;//数组乱序,更新flag表示对其进行操作
//交换相邻的两个数据
tmp=nums[j];
nums[j]=nums[j+1];
nums[j+1]=tmp;
}
}
//序列本身有序,直接退出循环
if(flag==false){
break;
}
}
}
int main(){
//数据输入
int n;
cin>>n;
vector<int> nums(n+1);
for(int i=1;i<=n;i++) {
cin>>nums[i];
}
bubbleSort(nums);
//数据输出
cout<<"排序结果如下:"<<endl;
for(int i=1;i<=n;i++){
cout<<nums[i]<<" ";
}
cout<<endl;
return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/
快速排序
快速排序 是一种基于分治思想 的排序算法,实现思路是选择一个基准元素,将待排序序列划分为两部分,其左边 元素小于等于基准元素,右边 元素大于等于 基准元素,然后递归对左右子数组重复该过程
快速排序步骤:
1.选择基准元素(常见选择策略有:选择第一个元素、选择最后一个元素、选择中间元素)
2.遍历待排序序列,将小于等于基准元素的元素放在基准元素左边,将大于等于基准元素的元素放在基准元素右边
3.递归,对基准元素左边的子序列和右边的子序列重复步骤1和步骤2直到子序列长度为1
快速排序的时间复杂度 :最好情况:O(nlogn) ;最坏情况:O(n^2) ;平均情况:O(nlogn)
快速排序代码实现如下(C++):(注:由于快速排序实现方法多样,此处仅选择挖坑法实现)
cpp
//快速排序(挖坑法)代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void Qsort(vector<int> &nums, int left, int right){
int key;//基准元素
if(left<right){//待排序数组中至少有两个数据
int i=left;
int j=right;
key=nums[left];//以最左边元素为基准元素(此时nums[left]的值被保存到key中,nums[left]的位置就变成了一个坑)
while(i<j){
//从后往前找比key小的数(填左坑)
while(i<j && nums[j]>key){
j--;
}
if(i<j){
nums[i]=nums[j];//将nums[j]填到左坑中,nums[j]的位置变成了坑
i++;
}
//从前往后找比key大的数(填右坑)
while(i<j && nums[i]<key){
i++;
}
if(i<j){
nums[j]=nums[i];//将nums[i]填到右坑中,nums[i]的位置又变成了新的坑
j--;
}
}
//循环结束后i==j,即左右指针指向同一位置
nums[i]=key;//将基准元素填到最后一个坑内
Qsort(nums,left,i-1);//对基准元素左边的子数组进行递归排序
Qsort(nums,i+1,right);//对基准元素右边的子数组进行递归排序
}
}
int main(){
//数据输入处理
int n;//数组大小
cin>>n;
vector<int> nums(n+1);
for(int i=1;i<=n;i++){
cin>>nums[i];
}
Qsort(nums,1,n);
//数据输出处理
cout<<"排序结果如下:"<<endl;
for(int i=1;i<=n;i++){
cout<<nums[i]<<" ";
}
cout<<endl;
return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/
归并(合并)排序
归并排序 ,也叫合并排序(下面统一叫归并排序)。归并排序是一种基于分治思想 的排序算法,实现思路是先将待排序序列递归差分为最小单元(单个元素),再将有序的子数组逐层合并, 最终得到完整的有序数组
归并排序步骤:
1.拆分:将待排序序列从中间拆分为左右两个子数组,然后递归拆分左右子数组,直到每个子数组仅含1个元素
2.合并:从最小的有序子序列开始,将两个有序子数组合并为一个新的有序序列,然后逐层向上合并,最终合并为一个完整的有序数组
归并排序的时间复杂度 :与待排序序列本身是否有序无关,时间复杂度恒为O(nlogn)
归并排序代码实现如下(C++):
cpp
//归并排序代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void mergeSort(vector<int> &nums,int left,int right){
if(left >= right)return;//递归出口
//分治操作
//将待排序序列拆分为两半,左半边为left~mid 右半边为mid+1~right
int mid = (left + right)/2;
//对左半边区间进行归并排序
mergeSort(nums,left,mid);
//对右半边区间进行归并排序
mergeSort(nums,mid+1,right);
//合并操作
int i=left;//左半边区间指针,初始指向本区间第一个元素
int j=mid+1;//右半边区间指针,初始指向本区间第一个元素
vector<int> tmp;//临时数组,暂存合并后的有序序列
//同时遍历左右区间,按顺序放入临时数组
while(i<=mid && j<=right){
//左区间元素更小,放入临时数组后左指针后移
if(nums[i]<=nums[j]){
tmp.push_back(nums[i++]);
}
//右区间元素更小,放入临时数组后右指针后移
else{
tmp.push_back(nums[j++]);
}
}
//处理左区间剩余元素(右区间已遍历完)
while(i<=mid){
tmp.push_back(nums[i++]);
}
//处理右区间剩余元素(左区间已遍历完)
while(j<=right){
tmp.push_back(nums[j++]);
}
//将临时数组中的有序序列放回原数组的[left,right]区间
copy(tmp.begin(), tmp.end(), nums.begin() + left);
}
int main(){
//数据输入
int n;
cin>>n;
vector<int> nums(n+1);
for(int i=1;i<=n;i++) {
cin>>nums[i];
}
mergeSort(nums,1,n);
//数据输出
cout<<"排序结果如下:"<<endl;
for(int i=1;i<=n;i++){
cout<<nums[i]<<" ";
}
cout<<endl;
return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/
总结
1. 上面提到的七种排序算法都属于内排序(待排序的所有数据都能一次性加载到计算机的内存中,并在内存中完成整个排序过程的排序方式)
2. 以稳定性(时间和空间复杂度是否恒定)看,在这七种排序算法中,
稳定的排序算法有:插入排序、冒泡排序、归并排序;
不稳定的排序算法有:希尔排序、选择排序、堆排序、快速排序;