五类八个排序比较+实现+详细解释

前言

刚学完数据结构,写此文目的如下

  • 加深对五大类共八个排序算法思想的理解
  • 比较各自的算法效率(主要是时间复杂度)
  • 寻找算法思想间的联系与区别
  • 用顺序结构、链式结构实现算法
  • 用递归、非递归(迭代)实现算法
  • 记录下来以便复习巩固

五类排序算法思维导图
**五类排序算法思维导图**

稳定性: 举个例子,假如我们要对序列A进行排序,排序前A中存在两个元素a1,a2,这两个元素满足同时条件1,a1=a2;条件2,a1在a2之前。若是在排好序的A’中a1依旧在a2之前,那么,我们可以称其满足稳定性。

一句话,稳定性就是遵循先来后到的原则

总结并补充上图:
1,常用排序有五大类,每一类中都是先出现简单,符合人思维的算法,为了提高效率,大家在此前基础上改进算法,进而出现了高效的算法。可见算法的发展是循序渐进,迭代而成,因此,若是按照其发展脉络学习算法,更容易理解算法间的关系
2,简单插入、交换、基数排序满足稳定性,其余皆不稳定
3,希尔排序效率与增量选取有关
4,堆排序有大、小顶堆之分。升序建大顶堆;降序建小顶堆

本文均以升序为例

数据结构

采用顺序结构,为了程序通用性,使用动态数组

typedef struct 
{
	int *elem;//数组首地址
	int length;//数组长度
}SqList;

//初始化动态数组 
void InitSqList(SqList &L)//记得加引用,赋初值才有效 
{
	L.elem = NULL;
	L.length = 0;
}

自定义队列及其基本操作

//队列数据结构
typedef struct QNode
{
	int data;
	struct QNode* next;
}QNode,LNode,*LinkList;

typedef struct
{
	QNode* front;
	QNode* rear;
}LinkQueue;
//初始化
void InitQueue(LinkQueue &Q)
{
	Q.front = Q.rear = (QNode*)malloc(sizeof(QNode));//头尾均指向头指针 
	Q.front->next = NULL;//尾部赋空 
}
//入队:尾部插入 
void Enqueue(LinkQueue &Q,int data)
{
	QNode* p = (QNode*)malloc(sizeof(QNode));
	p->data = data;
	p->next = NULL;
	
	Q.rear->next = p;
	Q.rear = p;
}
//判空:空->true;非空->false 
bool IsEmpty(LinkQueue Q)
{
	if(Q.front->next)	return false;
	else return true;
}
//出队:头部删除;注意删除的节点为尾节点时,重新将尾节点指向头结点 
void Dequeue(LinkQueue &Q)
{
	if(!IsEmpty(Q))
	{
		Q.front->next = Q.front->next->next;
		if(IsEmpty(Q))	Q.rear = Q.front;//删除尾节点,重新将尾指针指向头 
	}
}
//获取队头元素 
int GetTop(LinkQueue &Q)
{
	if(!IsEmpty(Q))
	{
		return Q.front->next->data;
	}
	else return -111111;//表示获取失败,即队空
}

前期准备函数

  • 由于使用文件交换数据,所以需设计文件存储、读取、重读取函数;随机产生不超过五位的非负整数
  • 由于两个元素间交换频繁,因此设计元素交换函数

//创建动态数组 
void CreateSqList(SqList &L)//记得加引用 
{	
	L.length = N;//N是宏定义,表示元素个数
	L.elem = (int*)malloc(N*sizeof(int));//动态数组必须分配内存!!!!! 
	fstream inFile("Sort.dat",ios::binary | ios::in);//以只读二进制方式存储文件
	inFile.read((char*)L.elem,N*sizeof(int));
	inFile.close();//记得关闭  
}
//重新加载动态数组,以相同的数据测试新的排序算法
void ReloadSqList(SqList &L)
{
	if(L.elem != NULL)
	{
		L.length = N;
//	L.elem = (int*)malloc(N*sizeof(int));//动态数组必须分配内存!!!!! 
		fstream inFile("Sort.dat",ios::binary | ios::in);//以只读二进制方式存储文件
		inFile.read((char*)L.elem,N*sizeof(int));//二进制读取,直接覆盖L之前的元素
		inFile.close();//记得关闭
	}
}
//把随机数以二进制形式存入文件。每次直接写会覆盖之前的内容 
void CreateRandFile()//文件流无法作为参数???或许是因为文件可以在任何处被打开,可看做全局数组 
{
	SqList L;//中间存储 
	InitSqList(L);
	L.length = N;
	L.elem = (int*)malloc(N*sizeof(int));
	srand((int)time(0));//随机数种子 
	for(int i = 0; i < N; i++)
	{	
	 	L.elem[i] = rand()%100000;//保证产生5位数以内,为桶排序准备 
	}
	fstream outFile("Sort.dat",ios::binary | ios::out);//以只写二进制方式存储文件
	outFile.write((char*)L.elem,N*sizeof(int));
	outFile.close();//记得关闭读,下次再打开读指针从头开始 
	free(L.elem);//记得释放 
} 
//交换 
void swap(int &a, int &b)//记得加引用,否则传入的参数在此函数结束后不会改变
{
	int t = a;
	a = b;
	b = t;
}
//仅仅为测试从文件读取数据是否成功,排序是否成功
void TraverseSqList(SqList L)
{
	for(int i = 0; i < L.length; i++)
	{
		cout<<L.elem[i]<<" ";
	}
}

插入排序

简单插入排序

算法思想

一个元素插入一个有序序列L,新序列L’依旧有序

算法描述

1,初始状态:第一个元素默认已有序,加入有序序列L
2,将距离序列L最近的元素插入L,得到新的有序序列L
3,重复步骤2,直到所有元素均在L中

算法实现(C++)

//=========================简单插入排序============================ 

//简单插入排序,从第二个开始 
void InsertSort(SqList L)
{
	for(int i = 1; i < L.length; i++)//从第二个开始 
	{
		for(int j = i; j > 0; j--)
		{
			if(L.elem[j] < L.elem[j-1])
			{
				swap(L.elem[j],L.elem[j-1]);
			}
			else break;//提高效率 
		}
	} 
}

希尔排序

算法思想

  • 1, 确定一个起点a1,选取与其间距为id的元素构成一个子序列,L1={a1,a1+d,…a1+id},对其进行简单插入排序,L1为有序
    在确定新的起点a2,选取与其间距为id的元素构成一个子序列,L2={a2,a2+d,…a2+id},对其进行简单插入排序,L2为有序
    以次类推,最终得到d个子序列,且每个子序列均有序
    因此,在L中间隔为d的元素已排好序
    (在同一个子序列中的元素下标必构成公差为d的等差数列)
  • 2, 减小d,重复步骤1,直至d=1

实例讲解

第一行为原始序列,d为增量
在这里插入图片描述

代码实现(C++)

tips:

  • 1,每个序列的第一个默认是排好序的
  • 2,一个序列分成dk个子序列时,
    • 思路一:传统是将i%dk相同的ai抽出来成为新序列,进而对该新序列使用简单插入排序,再处理下一个序列
    • 思路二:i%dk = {0,1,…dk-1} 在不同的子序列间来回处理,处理完余数为0,立刻处理余数为1,2,…dk-1。此思路为该程序的写法
  • 3,判断条件中一发现当前数更大,立刻break,否则时间复杂度可能比简单插入还高
  • 4,这是测试快速排序时发现的问题:数组下标越界,j>=dk,否则j-dk会溢出。很严重的问题是它溢出了照样输出,不终止,运行到快速排序就卡着了,误导我以为是快排出现问题。
  • 5,通过打印输出找问题时又发现了一个严重问题:即使不通过应用L,数组L.elem依旧被改变,原因在于虽然L作为形参传入,但L.elem是指针,代表着地址,在函数中对其操作,相当于直接在其对应地址改动
  • 6,要有自信,迅速定位问题位置,明白当前问题可能是由之前问题导致,连锁反应,可见封装的优越性。尽量不用递归,一是难调,二是耗空间
//=========================希尔排序============================ 

//希尔插入,按照一定的增量dk,进行插入排序 

void ShellInsert(SqList L, int dk)
{
	//=============思路一实现=========================== 
	for(int k = 0; k < dk; k++)
	{
		for(int i = k + dk; i < L.length; i += dk)//i%dk = {0,1,...dk} 间来回转换 
		{
		//	for(int j = i; j > 0; j -= dk)错误示例,会溢出 
			for(int j = i; j >= dk; j -= dk)
			{
				if(L.elem[j] < L.elem[j-dk])
				{
					swap(L.elem[j], L.elem[j-dk]);
				}
				else break;//不跳出时间复杂度比简单插入还高 
			}
		}
	} 
	
/* 
	//==================思路二实现================= 
	for(int i = 0 + dk; i < L.length; i ++)//i%dk = {0,1,...dk} 间来回转换 
	{
		for(int j = i; j >= dk; j -= dk)//方案一:控制j的范围,j>=dk,否则会溢出 
		{
		//	if(j - dk >= 0)//方案二:注意判断是否存在,否则j<dk时必溢出!!!!!!!!! 
		//	{
				if(L.elem[j] < L.elem[j-dk])//两种解决方案 
				{
					swap(L.elem[j], L.elem[j-dk]);
				}
		//	}
			else break;//不跳出时间复杂度比简单插入还高 
		}
	}*/
} 

void ShellSort(SqList L)
{
	int d[3] = {5,3,1};//增量数列 
	for(int i = 0; i < 3; i++)
	{
		ShellInsert(L,d[i]);
	}
//	TraverseSqList(L);
}

交换排序

简单交换(冒泡)排序

算法思想

两两比较,较大者往后走/较小者往前走

实现代码(C++)

//通过两两交换,求得最小值,从前往后填入 
void BubbleSort(SqList L)
{
	for(int i = 0; i < L.length - 1; i++)
	{
		for(int j = i+1; j < L.length; j++)
		{
			if(L.elem[i] > L.elem[j])
			{
				swap(L.elem[i],L.elem[j]);
			}
		}
	} 
//	TraverseSqList(L); 
} 

快速排序

算法思想

分割(核心):选取一个分割元素p,令p左边元素均小于等于p,p右边元素均大于等于p,因此,p的位置唯一确定。
因此,只需进行n次分割就可将长度为n的序列L排成有序序列

代码实现(C++)

递归版

//选择一个值pivot作为轴对数列进行分割,左小右大,分割完成,pivot位置确定
//快速排序是对不断对序列进行分割,每分一次,确定一个值的位置 
int Partition(SqList L,int low,int high)
{
	int i,j;//好习惯:尽量不改变参数 
	i = low;//头 
	j = high;//尾 
	
	int t = L.elem[low];//保存第一个,腾出空位 
	while(i < j)
	{
		while(i < j && t <= L.elem[j])	j--;//从j向前找到第一个比t小的值 
		L.elem[i] = L.elem[j];//填入空中 ,此时i的位置腾出 
		while(i < j && t >= L.elem[i])	i++;//从i向后找到第一个比t大的值
		L.elem[j] = L.elem[i];//填入空中 ,此时j的位置腾出
	}//跳出循环,i=j,且还必有一个空 
	L.elem[i] = t;//将开始的轴值填入最后一个空,该值位置确定,其左边均比它小,右边均比他大。 
	return i;//返回中轴的位置 
} 
//递归实现分割 
void QSort(SqList L,int low,int high)
{
	if(low < high)
	{
		int pivot  = Partition(L,low,high);
		QSort(L,low,pivot-1);
		QSort(L,pivot+1,high);
	}
}
//为了形式统一,接口一致,才写了这个函数 
void QuickSort(SqList L)
{
	QSort(L,0,L.length - 1);	
}

选择排序

算法思想

每趟选取一个最小值,往前放/每天选一个最大值,往后放

实现代码(C++)

//每趟选出一个最小值,从前往后放入第一个非有序位置 
void SelectSort(SqList L)
{
	for(int i = 0; i < L.length; i++)
	{
		int k = i;
		for(int j = i + 1; j < L.length; j++)
		{
			if(L.elem[k] > L.elem[j])	k = j;//记录最小值下标 
		}
		if(k != i)//如果ai不是最小值 ,交换ak,ai,使ai成为最小值 
		{
			swap(L.elem[k],L.elem[i]);
		}
	}
} 

堆排序(大顶堆)

大顶堆是完全二叉树,且任一子树根均大于左右孩子

算法思想

1,筛选(核心):堆排序关键是如何筛选出最大元素,可分为两种情况

  • 1,完全二叉树除了根,左右子树均已为大顶堆
  • 2,除了情况1的情况

A,第一种情况(好办)

  • 1,令根a与其左右孩子中较大者a’比较,若a>=a’,无需再比;若a<a’,交换这两个节点
  • 2,重复1,直至a为叶子

B,第二种情况(转化分解)
可以将情况二转化为多个情况一,从而解决问题。
倒过来反复使用解决方案A

  • 1,完全二叉树的叶子左右子树均为大顶堆,所以可用A筛选
  • 2,从最后向前一步步筛选,所以每个即将要筛选的节点的左右子树均已为大顶堆,所以均可使用A筛选

2,建大顶堆
使用反复筛选建立大顶堆,来一个元素,筛选一次。
3,排序
建好大顶堆后,将根元素与最后一个元素交换,同时完全二叉树去除最后一个元素,再利用筛选,得到元素少一个的新大顶堆,重复直至仅有一个元素即可。

实现代码(C++)

//大顶堆--》升序;小顶堆-》降序 
//筛选函数,使用前提:根的左右子树都是堆,仅根破坏了大顶堆的定义 
void HeapAdjust(SqList L,int low,int high) 
{
	int i,j;
	i = low; 
	while(2*i <= high)//堆是完全二叉树:左子堆存在 
	{
		j = 2*i;//初值 ,每次需更新 
		if(2*i+1 <= high && L.elem[2*i] < L.elem[2*i+1])	j = 2*i+1;//若右子堆也存在,选择左右大者 
		if(L.elem[i] < L.elem[j])	swap(L.elem[i],L.elem[j]);//若ai<aj,交换 
		else break;//否则立刻跳出,提升效率 
		i = j;//更新i 
	}
//	TraverseSqList(L);
} 
//乱序数列通过从最后一个开始向前反复筛选可变为堆 
//0号单元不用 
void HSort(SqList L,int low,int high)
{
	for(int i = high; i >= 1; i--)//整理成大顶堆 
	{
		HeapAdjust(L,i,high);
	} 
//	cout<<"12:";TraverseSqList(L);
	int i,j;
	i = low;
	j = high;
	//for(int k = 1; k < L.length; k++)//次数必须为n-1次,多一次少一次都不行 ???
	while(true)
	{//因为0号单元有元素,但是堆不用,0*x=0。所以排序次数多了会把0号元素卷入排序,导致错误
	//方案一:严格控制次数:n-1次
	//方案二:利用j=0跳出循环 
		if(j == 0)break;
		swap(L.elem[i],L.elem[j]);
		j--;
		HeapAdjust(L,i,j); 
	}
} 
//与快排相同,该函数仅是为了接口统一 
void HeapSort(SqList L)
{
	HSort(L,1,L.length-1);
}

归并排序

两个有序序列合并成一个有序序列

代码实现(C++)

  • 递归与非递归合并算法一致
  • 递归简洁,但效率一般不如非递归高,且不易调试

合并两条有序序列

//=========================归并============================ 
void Merge(SqList &L1,int low,int m,int high)
{	
	int i,j,k=low;//错误示范:k=0。!!每次调用归并起点并不都是0 
	i = low;
	j = m+1;
	
	SqList L2;
	L2.length = L1.length;
	L2.elem = (int*)malloc(N*sizeof(int));
	while(i <= m && j <= high)//两条序列均未走完
	{
		if(L1.elem[i] < L1.elem[j])	L2.elem[k++] = L1.elem[i++];
		else L2.elem[k++] = L1.elem[j++];
	}
	//若是有一序列还未走完,直接将剩余部分全部赋给新序列
	while(i <= m)	L2.elem[k++] = L1.elem[i++];
	while(j <= high)  L2.elem[k++] = L1.elem[j++];
	for(int t = low; t <= high; t++)//low->high 才需赋值 
	{
		L1.elem[t] = L2.elem[t];
	}
	free(L2.elem); 
}

递归形式

//递归形式 
void MSort(SqList L,int low,int high)
{
	if(low >= high )	return;
	int m = (low + high)/2;
	
	MSort(L,low,m);
	MSort(L,m+1,high);
	Merge(L,low,m,high);
}
//递归形式
void MergeSort_Recursion(SqList L)
{
	MSort(L,0,L.length-1);		
}

非递归:递推方程

//非递归:递推方程 
void MergeSort_Iteration(SqList L)
{
	int low,m,high;
	for(int dk = 1; dk <= (L.length+1)/2; dk *= 2)//每个子序列个数,1~2的n次方 
	{
		for(low = 0; low < L.length - 2*dk + 1; low = low+2*dk)//控制次数,high>low,避免溢出,最后若有剩余一部分等最后再处理 
		{
			m = dk + low - 1;
			high = 2*dk + low - 1;
			Merge(L,low,m,high);
		}
	} 
	if(high < L.length - 1)//处理之前剩余的部分,在来一次归并 
	{
		Merge(L,0,high,L.length-1);
	}		
}

基数排序

算法思想

以小于100000的自然数(五位数)举例
准备十个队列,分别表示编号0~9
分配:令关键字为个位,遍历L的同时,令个位为i的数进入第i个队列
收集:以第0个队列为首,将0~9个队列头尾相接,成为新序列
一次分配对应一次收集,依次对十位,百位,千位,万位进行分配,收集

实现代码(C++)

自定义队列

//数组到链式的转换 
void SqToLink(SqList L1,LinkList &L)
{
	L = (LinkList)malloc(sizeof(LNode));
	L->next = NULL;
	
	LNode* p;
	for(int i = 0; i < L1.length; i++)//头插法 
	{
		p = (LNode*)malloc(sizeof(LNode));
		p->data = L1.elem[i];
		
		p->next = L->next;
		L->next = p;
	}
}
//遍历链式序列,测试使用 
void TraverseLinkList(LinkList L)
{
	LNode* pcur = L->next;
	while(pcur)
	{
		cout<<pcur->data<<" ";
		pcur = pcur->next;
	}
}
//基数排序的按位分配 
void Distribute(LinkList &L,LinkQueue* Q,int n)
{
	QNode* pcur = L->next;

	while(pcur)
	{
		int a = pcur->data;
		Enqueue(Q[(a/n)%10],a);
		L->next = pcur->next;
		free(pcur);
		pcur = L->next;
	}
}
//收集队列 : 
void Collect(LinkList &L,LinkQueue* Q)
{
	QNode* plast = L; 
	for(int i = 0; i < 10; i++)
	{
		if(!IsEmpty(Q[i]))
		{
			plast->next = Q[i].front->next;
			plast = Q[i].rear;
			Q[i].front->next = NULL;//注意对队列的清空,为下一次分配准备 
			Q[i].rear = Q[i].front;
		}
	}
}
//基数排序 
void RadixSort(SqList L)
{
	LinkList L2;
	SqToLink(L,L2);
//	TraverseLinkList(L2);cout<<endl; 
	LinkQueue Q[10];
	for(int i = 0; i < 10; i++)
	{
		InitQueue(Q[i]);
	}
//	TraverseLinkList(L2);
	for(int i = 1; i <= 10000; i *= 10)
	{
		Distribute(L2,Q,i);
		Collect(L2,Q);
	//	cout<<"collect:";TraverseLinkList(L2);cout<<endl;
	}//TraverseLinkList(L2);
}

测试

测试数据约定

  • 共10组数据,每组有20000个不大于99999的随机自然数
  • 第一组为降序,第二组升序,剩余八组随机乱序

测试结果

  • 第一组降序
    在这里插入图片描述
  • 第二组升序
    在这里插入图片描述
  • 第三组乱序
    在这里插入图片描述
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值