数据结构6——八大排序

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

1.思想

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

2.代码实现

①通过双重循环实现

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

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

void Bubble_Sort(int brr[],int len)
{
	//双重循环
	//外层循环表示趟数  len-1趟
	//内层循环表示每趟比较的次数,受到外层循环的影响
	for(int i=0;i<len-1;i++)
	{
		for(int j=0;j<len-1-i;j++)
		{
			if(arr[j]>arr[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;i<len-1;i++)
	{
		tag=true;
		for(int j=0;j<len-1-i;j++)
		{
			if(arr[j]>arr[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<len-1;i++)
	{
		int min=i;//定义一个变量表示最小值位置
		for(int j=i+1;j<len;j++)//内层循环表示比较的次数  
		{
			if(arr[j]<arr[min])  //实现找最小的元素所在的下标 
			{
				min=j;//逐个比较,较小则赋予给min,直到min此时为最小 
				
			}
		}
		//找到了则与第一个元素进行交换
		//前提:最小值不是第一个元素,如果是,则不需要交换 
		if(min!=i)
		{
			int temp=arr[min];
			arr[min]=arr[i];
			arr[i]=temp;
		 } 
		 
	 } 
}

三、直接插入排序

1.思想

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

2.代码实现

①双重循环实现

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

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

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

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

void Insert_Sort(int arr[],int len) 
{
	for(int i=1;i<len;i++)//外层循环表示趟数(待插入的数据的个数) 
	{
		int temp=arr[i];//待插入的数据 
		int j=0;//注意:如果定义在内层循环中,当退出内层循环时,外层循环无法识别(局部变量作用域问题) 
		for(j=i-1;j>=0;j--) //内层循环表示已排好序列的数据 (从右向左) 
		{
			if(temp<arr[j]) //如果待插入的元素值小于排好序的元素值,则排好序的元素值向后挪动
			{
				arr[j+1]=arr[j]; 
			} 
			else         //否则,则将待排好序的元素插入到其之后,与触底情况相同 
			{
				//arr[j+1]=temp;
				break;	
			}
		} 
		//如果j=-1 ,表示触底了,将待排好序的元素插入到其之后 
		arr[j+1]=temp; 
	 } 
} 

四、基数排序(桶排序)

1.思想

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

2.代码实现

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

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

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

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

//求位数函数 
int Get_digit(int arr[],int len)
{
	//1.求最大值 
	int max=arr[0];
	for(int i=1;i<len;i++)
	{
		if(arr[i]>max)
		{
			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<gap;i++)
	{
		number=number/10;
	}
	return number%10;
}
//Radix 排序操作
void  Radix(int arr[],int len,int gap)
{
	//1.先申请10个桶,队列
	queue<int> Bucket[10];
	//2.将元素按照对应的位数依次放入到对应的桶中
	for(int i=0;i<len;i++)
	{
		int index=Get_num_corresponding_Bucket(arr[i],gap);
		Bucket[index].push(arr[i]);
	}
	//3.将元素从桶中取出
	int k=0;
	for(int i=0;i<10;i++)
	{
		while(!Bucket[i].empty())
		{
			arr[k++]=Bucket[i].front();
			Bucket[i].pop();
		}
	 } 	
}

void Radix_Sort(int arr[],int len) 
{
	//得到最大值的位数 ----趟数 
	int digit=Get_digit(arr,len);
	//每一趟都要调用基数函数 
	for(int i=0;i<digit;i++)
	{
		Radix(arr,len,i);//第三个参数表示按哪一位 ==》》 i=0,表示按个位 
	} 
}

五、希尔排序

1.思想

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

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

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

①增量由大到小给值。

②增量之间尽量互素。

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

2.代码实现

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

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

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

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

void Shell(int arr[],int len,int gap)
{
	for(int i=gap;i<len;i++) //根据增量,对于每一个分组,认为首元素均是有效的 
	{
		int temp=arr[i];
		int j=0;
		for(j=i-gap;j>=0;j=j-gap)  //此时,每个组移动的距离=gap 
		{
			if(temp<arr[j])
			{
				arr[j+gap]=arr[j];
			}
			else
			{
				break;
			} 
		}
		arr[j+gap]=temp;
	}
}

void Shell_Sort(int arr[],int len)
{
	//先给出一个增量数组
	int gap[]={5,3,1};
	int len_gap=sizeof(gap)/sizeof(gap[0]);
	for(int i=0;i<len_gap;i++) //表示趟数 
	{
		Shell(arr,len,gap[i]);
	} 
 } 

六、快速排序

1.思想

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

2.代码实现

(1)递归方法实现

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

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

③划分:

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

int Partition(int arr[],int left,int right)
{
	//取第一个值为基准值 
	int temp=arr[left];
	while(left<right)   
	{
		//先从右边界(right)按从右向左的顺序出发,最右边的值大于临时变量,right--;
      	//因为right要进行移动,所以要判断left和right的关系,防止越界 
		while(left<right&&arr[right]>temp)
		{
			right--;
		}
		//循环退出条件1:两个指针相遇,表示此位置即为基准值所在的位置,基准值此时已排序完成。
		if(left==right)  
		{
			break;//跳出外层循环 
		}
		//循环退出条件2:arr[right]<=temp 表示需要将此值存放在左边left处的位置 
		arr[left]=arr[right];
		
		//左边界(left)按从左到右的顺序出发,左边的值小于临时变量,则left++; 
		//因为left要进行移动,所以要判断left和right的关系,防止越界 
		while(left<right&&arr[left]<=temp)
		{
			left++;
		}
		//循环退出条件1:两个指针相遇,表示此位置即为基准值所在的位置,基准值此时已排序完成。
		if(left==right)
		{
			break;
		}
		//循环退出条件2:arr[left]>temp 表示需要将此值存放在右边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<par-1)  //保证左边至少有2个值,因为至少存在2个值才需要排序 
	{
		Quick(arr,left,par-1);
	}
	if(par+1<right) //保证右边至少有2个值,因为至少存在2个值才需要排序 
	{
		Quick(arr,par+1,right);
	}
}
void Quick_Sort(int arr[],int len)
{
	Quick(arr,0,len-1);
 } 

(2)非递归的方式实现

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

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

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

int Partition(int arr[],int left,int right)
{
	//取第一个值为基准值 
	int temp=arr[left];
	while(left<right)   
	{
		//先从右边界(right)按从右向左的顺序出发,最右边的值大于临时变量,right--;
      	//因为right要进行移动,所以要判断left和right的关系,防止越界 
		while(left<right&&arr[right]>temp)
		{
			right--;
		}
		//循环退出条件1:两个指针相遇,表示此位置即为基准值所在的位置,基准值此时已排序完成。
		if(left==right)  
		{
			break;//跳出外层循环 
		}
		//循环退出条件2:arr[right]<=temp 表示需要将此值存放在左边left处的位置 
		arr[left]=arr[right];
		
		//左边界(left)按从左到右的顺序出发,左边的值小于临时变量,则left++; 
		//因为left要进行移动,所以要判断left和right的关系,防止越界 
		while(left<right&&arr[left]<=temp)
		{
			left++;
		}
		//循环退出条件1:两个指针相遇,表示此位置即为基准值所在的位置,基准值此时已排序完成。
		if(left==right)
		{
			break;
		}
		//循环退出条件2:arr[left]>temp 表示需要将此值存放在右边right处的位置 
		arr[right]=arr[left]; 
	}
	arr[left]=temp;
	return left;
}
void Quick_No_Recursion(int arr[],int left,int right)
{
	//申请一个栈 
	std::stack<int>st;
	//将左边界和右边界压入栈中
	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<par-1) //注意一下,以划分函数的左右边界来比较 
		{
			st.push(tmp_left);
			st.push(par-1);
		}
		if(par+1<tmp_right)
		{
			st.push(par+1);
			st.push(tmp_right);
		}
	 } 
	
}
void Quick_No_Recurison_Sort(int arr[],int len)
{
	Quick_No_Recursion(arr,0,len-1);
 } 

七、归并排序

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<right)
	{
		middle=(left+right)/2; //从中间位置断开,划分 
		Divide(arr,left,middle,brr);//左半部分递归 
		Divide(arr,middle+1,right,brr);//右半部分递归 
		Merge(arr,left,middle,right,brr);//递归完成,数据变为一个个时,执行Merge 
	} 
} 

void Merge_Sort(int arr[],int len) 
{
	//重新申请一个数组,用来表示左右区间合并后的序列
	int* brr=(int*)malloc(sizeof(int)*len);
	//对于malloc申请的要进行判断是否申请成功
	if(brr==NULL)
		exit(1); 
	//分治
	Divide(arr,0,len-1,brr); 
	//malloc 申请的空间要进行释放
	free(brr); 
	
}

八、堆排序

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<arr[left])//始终让左孩子存放较大值,与根节点交换 
		{
			arr[start]=arr[left];
			start=left; //让根节点==左孩子,对受影响的进行调整	
		 } 
	 	else //表示根节点大于左右孩子 
	 	{
	 		//arr[start]=temp;
	 		break;
	 	}
	 }
	 //跳出for循环,表示此时结点为叶子节点
	  arr[start]=temp;
} 
void Heap_Sort(int arr[],int len) 
{
	//1.将数组想象成一个完全二叉树,找出最后一个非叶子 
	//最后一个节点 len-1      //父节点: ((len-1)-1)/2
	//2.首次调整,需要由右向左,由下向上进行
	for(int i=(len-1-1)/2;i>=0;i--)
	{
		//第二个和第三个参数表示开始的范围和结束的范围。
		//由于结束的范围都不相同,所以可以设计为len-1 
		Adjust(arr,i,len-1); 
	} 
	//3.调整后,交换根节点(最大值)与最后一个元素的位置 
	for(int i=0;i<len-1;i++)  //表示趟数  元素个数为n,比较趟数n-1趟 
	{
		//交换 
		int temp=arr[0];
		arr[0]=arr[len-1-i];
		arr[len-1-i]=temp;
		//交换后,将最后一个节点断开并进行调整(只调整外层) 
		Adjust(arr,0,len-1-i-1);  
	 } 
}

九、总结

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

排序算法时间复杂度    空间复杂度         稳定性
冒泡排序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)

不稳定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值