数据结构6——八大排序

一、冒泡排序(沉石排序)

1.思想

每一趟排序,通过两两比较后交换较大值,使得最大值放到末尾。

2.代码实现

①通过双重循环实现

②外层循环:表示趟数。如果假设元素个数为n,则外层循环的趟数为n-1。

③内层循环:表示比较的次数。受到外层循环的影响。

void Bubble_Sort(int brr[],int len)
{
	//双重循环
	//外层循环表示趟数  len-1趟
	//内层循环表示每趟比较的次数,受到外层循环的影响
	for(int i=0;iarr[j+1])//比较:左边元素大于右边元素,则交换
			{
				int temp=arr[j];
				arr[j]=arr[j+1];
				arr[j+1]=temp; 
			 } 
		 } 
	 } 
	 
}

3.冒泡排序的优化 

假设通过前几趟比较后数据已经有序,但根据冒泡排序算法,仍然需要比较后几趟直到结束。但此时后几趟的比较无意义。

所以,可以对冒泡排序算法进行优化,使其一旦有序(也就是不发生元素之间的交换)后,不再进行后几趟操作。可以通过设计标记位判断数据是否有序。

void Bubble_Sort2(int brr[],int len)
{
	//双重循环
	//外层循环表示比较的趟数  len-1趟
	//内层循环受到外层循环的影响
	bool tag=true; //设计标记位,判断数据是否有序
	for(int i=0;iarr[j+1])//比较:左边元素大于右边元素,则交换
			{ 
				int temp=arr[j];
				arr[j]=arr[j+1];
				arr[j+1]=temp; 
                tag=false; 
			 }
		 }
		 if(tag)
		 	return ; 
	 } 
	 
} 

二、选择排序

1.思想

每一趟从待排序序列中找到最小值,与待排序序列的第一个元素交换,此时,第一个元素有序。待排序序列元素个数-1,继续重复,直到待排序序列元素个数为1结束。

2.代码实现

①双重循环实现

②外层循环:表示趟数。如果假设元素个数为n,则外层循环的趟数为n-1。

③内层循环:表示比较的次数。

注意:先找到最小值元素的所在下标,交换时,只交换最小值和第一个元素,其余均不交换。

如果最小值元素本身就是第一个元素,则不需要交换,因为无意义。

void Select_Sort(int arr[],int len) 
{
	//外层循环表示趟数   len-1趟
	for(int i=0;i

三、直接插入排序

1.思想

将序列划分为有序序列和待排序序列2部分,认为第一个元素是有序的。每一趟从待排序的序列中取第一个值,将其与已排好序的元素按从右向左的顺序依次比较,如果已排好序的元素大于插入元素则向后挪动,如果小于等于插入的值,或者触底则将待插入的元素插入到其后。

2.代码实现

①双重循环实现

②外层循环:表示趟数。假设有n个元素,则从1开始到n结束。

③内层循环:表示比较的次数。从最右边已排好序元素开始到0结束。

注意:对于小于等于插入的值,或者触底的情况,可以将两种情况的代码合成。

(在进行合成的时候,要注意局部变量的作用域问题)

void Insert_Sort(int arr[],int len) 
{
	for(int i=1;i=0;j--) //内层循环表示已排好序列的数据 (从右向左) 
		{
			if(temp

四、基数排序(桶排序)

1.思想

对待排序的序列,首先判断最大值的位数,然后先从每个元素的个位开始进行排序,并将按个位排序后的序列,再按十位进行排序,结束的标志是最大值的位数。

2.代码实现

①首先需要获取最大值的位数,根据最大值的位数,可以得出需要的趟数。

②需要申请10个桶(队列)来存放对应的值。此时要将各个值按照位数放入到对应的桶中。

(因为无论是按个位还是十位等排序,只能是0-9号下标,并且要满足先进先出)

③将桶中的元素取出放入到原数组中。(因为下一趟的排序需要依靠上一次排序后的结果)

//求位数函数 
int Get_digit(int arr[],int len)
{
	//1.求最大值 
	int max=arr[0];
	for(int i=1;imax)
		{
			max=arr[i];
		}
	}
	//2.求最大值的位数
	int count=0;
	while(max!=0)
	{
		max=max/10;
		count++;
	} 
	return count;
}
//获取元素对应的位数
int Get_num_corresponding_Bucket(int number,int gap) 
{
	for(int i=0;i Bucket[10];
	//2.将元素按照对应的位数依次放入到对应的桶中
	for(int i=0;i

五、希尔排序

1.思想

希尔排序是对直接插入排序的优化。

根据增量的大小,将数据分为对应个数的组,然后对每一分组单独进行直接插入排序,让每个小组内保证有序,继续按照下一个增量的大小进行分组排序,直到最后一个增量1,将全部数据分到一个组内,保证组内有序即可。

对于增量,用数组来保存。增量数组的特点:

①增量由大到小给值。

②增量之间尽量互素。

③增量数组的最后一个增量值必须是1。(才能保证全部数据都有序)

2.代码实现

每一组虽然是隔离的,但是想象成在一个组内。

①先确定增量数组。根据增量数组可以得到趟数。(增量的大小==分组的个数)

②每一趟都有对应的分组,对于每个组来说,认为每个组的第一个元素有序,所以外层循环开始位置==增量。

③内层循环从最右边已排好序元素开始到0结束,对于每个组来说,无论是与已排好序的元素比较,还是插入元素,移动的距离都=gap。

void Shell(int arr[],int len,int gap)
{
	for(int i=gap;i=0;j=j-gap)  //此时,每个组移动的距离=gap 
		{
			if(temp

六、快速排序

1.思想

每一趟,将待排序的第一个值作为基准,通过一趟排序,可以将待排序数据分为两半,左边的一半小于等于基准值,右边的一半大于基准值,然后对左右两半分别递归进行排序,直到数据全部有序。基准值两边划分好后,自身是有序的,它就在最终排序好所在的位置上。

2.代码实现

(1)递归方法实现

①划分的时候涉及到左右边界

②每一趟排序,都会获取一个已排好序的数据位置,根据此位置,再次进行左右区间的划分。

③划分:

首先,定义一个临时变量存储基准值(第一个值,左边界)。
然后,先从右边界(right)按从右向左的顺序出发,如果最右边的值大于临时变量,则 right--;如果小于等于临时变量,则将此值存放在左边left处的位置
接着,从左边界(left)按从左到右的顺序出发,如果左边的值小于等于临时变量,则left++; 如果大于临时变量, 则将此值存放在右边right处的位置
最后,如果left==right,即两个指针相遇,表示此位置即为基准值所在的位置,基准值此时已排序完成。
采用递归的方法,以基准值的左边作为左递归的右边界,以基准值的右边作为右递归的左边界。
递归的前提:left

int Partition(int arr[],int left,int right)
{
	//取第一个值为基准值 
	int temp=arr[left];
	while(lefttemp)
		{
			right--;
		}
		//循环退出条件1:两个指针相遇,表示此位置即为基准值所在的位置,基准值此时已排序完成。
		if(left==right)  
		{
			break;//跳出外层循环 
		}
		//循环退出条件2:arr[right]<=temp 表示需要将此值存放在左边left处的位置 
		arr[left]=arr[right];
		
		//左边界(left)按从左到右的顺序出发,左边的值小于临时变量,则left++; 
		//因为left要进行移动,所以要判断left和right的关系,防止越界 
		while(lefttemp 表示需要将此值存放在右边right处的位置 
		arr[right]=arr[left]; 
	}
	arr[left]=temp;
	return left;
}

void Quick(int arr[],int left,int right)
{
	int par=Partition(arr,left,right);//得到已排好序的值的位置 
	if(left

(2)非递归的方式实现

①定义一个栈,将左右边界压入栈中。

②判断栈是否为空,栈不为空,则取出两个边界,然后调用划分函数(partition),找到已排好序的位置,再次划分左右新的区间,压入栈中。

③重复执行,直到栈为空。

int Partition(int arr[],int left,int right)
{
	//取第一个值为基准值 
	int temp=arr[left];
	while(lefttemp)
		{
			right--;
		}
		//循环退出条件1:两个指针相遇,表示此位置即为基准值所在的位置,基准值此时已排序完成。
		if(left==right)  
		{
			break;//跳出外层循环 
		}
		//循环退出条件2:arr[right]<=temp 表示需要将此值存放在左边left处的位置 
		arr[left]=arr[right];
		
		//左边界(left)按从左到右的顺序出发,左边的值小于临时变量,则left++; 
		//因为left要进行移动,所以要判断left和right的关系,防止越界 
		while(lefttemp 表示需要将此值存放在右边right处的位置 
		arr[right]=arr[left]; 
	}
	arr[left]=temp;
	return left;
}
void Quick_No_Recursion(int arr[],int left,int right)
{
	//申请一个栈 
	std::stackst;
	//将左边界和右边界压入栈中
	st.push(left);
	st.push(right);
	//判断栈是否为空,不为空则将两个边界条件取出,取出后进行划分 
	while(!st.empty())
	{
		//先进后出,右边界先出 
		int tmp_right=st.top();
		st.pop();
		int tmp_left=st.top();
		st.pop(); 
		int par=Partition(arr,tmp_left,tmp_right); 
		if(tmp_left

七、归并排序

1.思想(分治)

先将长度为n的序列,通过划分,使其变为n个长度为1的序列,然后合并,合并为n/2个长度为2的有序组,然后接着合并,直到所有数据都合并到同一个组为止。

2.代码实现

①划分:使其变为n个长度为1的序列。

②合并:将数据不断进行合并,直到数据合并到同一个组为止。

void Merge(int arr[],int left,int middle,int right,int brr[]) 
{
	//将左区间数据与右区间的数据合并
	//左区间:[left,middle]   右区间:[middle+1,right]
	//定义两个变量,分别从左右区间的第一个元素开始 
	int i=left;
	int j=middle+1;
	//此变量表示的是brr数组中的位置 
	int k=left;  //因为可能待合并的两个区别只是原数组的一部分,所以对于brr来说,让其从left开始 
	//比较两个区间的数据,谁小谁先动,要申请一个数组
	while(i<=middle&&j<=right) //注意边界:边界均可达到 
	{
		if(arr[i]<=arr[j])
		{
			brr[k]=arr[i];
			i++;
			k++;
		}
		else
		{
			brr[k]=arr[j];
			j++;
			k++;
		}
	 }
	 //循环结束表示有一个走出范围 
	 while(i<=middle)
	 {
	 	brr[k++]=arr[i++];
	 } 
	 while(j<=right)
	 {
	 	brr[k++]=arr[j++];
	 }
	 //合并完成后,再将brr中两个组的合并结果重新挪动到arr中
	 for(int i=left;i<=right;i++)
	 {
	 	arr[i]=brr[i];
	  } 
}

//分治
void Divide(int arr[],int left,int right,int brr[])
{
	int middle=0; 
	if(left

八、堆排序

1.思想

每次确定一个最大值(通过维护大顶堆实现,因为大顶堆的根结点是所有值的最大值),然后与最后一个位置进行交换。

(1)已知父节点的下标求孩子节点的下标(父推子)

公式:

父节点下标 i

孩子节点下标 2i+1   2i+2

(2)已知孩子节点的下标求父节点的下标(子推父)

公式:

孩子节点的下标    i  

父节点的下标    (i-1)/2 

2.代码实现

①将数组想象成完全二叉树

②将完全二叉树调整为一个大顶堆

③将大顶堆的根节点与最后一个节点进行交换,交换完成后,断开这个节点,使其不参与后续的排序。

④重新调整为大顶堆,直到大顶堆中剩下一个节点。

注意:调整大顶堆

首次调整由内向外:从最后一个非叶子节点作为根节点的分支二叉树开始调整,然后从右到左,从上到下。

交换后重新调整,只需要调整一次,不需要向首次从内向外调整那样。

void Adjust(int arr[],int start,int end)
{
	//1.定义一个临时变量来存放待调整的非叶子结点 
	int temp=arr[start];
	//2.找出左右孩子
	int left=2*start+1;
	//3. 调整时,如果调整到较高层,可能会影响下层已调整好的树 
	//所以,需要for循环,从上向下进行判断 
	for(;left<=end;left=2*start+1) //进入循环,左孩子一定存在 
	{
		//4.判断根节点右孩子是否存在,以及让左孩子存放较大的值
		//退出if条件:
		//(1)右孩子存在,但左孩子的值大于右孩子
		//(2)右孩子不存在,但左孩子一定存在,因为左孩子在for循环中已经判断 
		if(left+1<=end&&arr[left+1]>arr[left])
		{
			left++; // 左孩子的值小于右孩子,就将右孩子的值赋值给左孩子 
		}
		if(temp=0;i--)
	{
		//第二个和第三个参数表示开始的范围和结束的范围。
		//由于结束的范围都不相同,所以可以设计为len-1 
		Adjust(arr,i,len-1); 
	} 
	//3.调整后,交换根节点(最大值)与最后一个元素的位置 
	for(int i=0;i

九、总结

不同排序算法的时间复杂度、空间复杂度、稳定性如下:

排序算法 时间复杂度     空间复杂度          稳定性
冒泡排序 O(n^2) O(1) 稳定
选择排序 O(n^2) O(1) 不稳定
直接插入排序 O(n^2) O(1) 稳定
基数排序 O(d(n+r)) O(n) 稳定
希尔排序 O(n^1.3~n^1.7) O(1) 不稳定
快速排序(递归)

O(nlogn)

O(logn) 不稳定
归并排序 (递归) O(nlogn) O(n) 稳定
堆排序 O(nlogn) O(1)

不稳定

你可能感兴趣的:(数据结构)