每一趟排序,通过两两比较后交换较大值,使得最大值放到末尾。
①通过双重循环实现
②外层循环:表示趟数。如果假设元素个数为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;
}
}
}
}
假设通过前几趟比较后数据已经有序,但根据冒泡排序算法,仍然需要比较后几趟直到结束。但此时后几趟的比较无意义。
所以,可以对冒泡排序算法进行优化,使其一旦有序(也就是不发生元素之间的交换)后,不再进行后几趟操作。可以通过设计标记位判断数据是否有序。
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结束。
①双重循环实现
②外层循环:表示趟数。如果假设元素个数为n,则外层循环的趟数为n-1。
③内层循环:表示比较的次数。
注意:先找到最小值元素的所在下标,交换时,只交换最小值和第一个元素,其余均不交换。
如果最小值元素本身就是第一个元素,则不需要交换,因为无意义。
void Select_Sort(int arr[],int len)
{
//外层循环表示趟数 len-1趟
for(int i=0;i
将序列划分为有序序列和待排序序列2部分,认为第一个元素是有序的。每一趟从待排序的序列中取第一个值,将其与已排好序的元素按从右向左的顺序依次比较,如果已排好序的元素大于插入元素则向后挪动,如果小于等于插入的值,或者触底则将待插入的元素插入到其后。
①双重循环实现
②外层循环:表示趟数。假设有n个元素,则从1开始到n结束。
③内层循环:表示比较的次数。从最右边已排好序元素开始到0结束。
注意:对于小于等于插入的值,或者触底的情况,可以将两种情况的代码合成。
(在进行合成的时候,要注意局部变量的作用域问题)
void Insert_Sort(int arr[],int len)
{
for(int i=1;i=0;j--) //内层循环表示已排好序列的数据 (从右向左)
{
if(temp
对待排序的序列,首先判断最大值的位数,然后先从每个元素的个位开始进行排序,并将按个位排序后的序列,再按十位进行排序,结束的标志是最大值的位数。
①首先需要获取最大值的位数,根据最大值的位数,可以得出需要的趟数。
②需要申请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。(才能保证全部数据都有序)
每一组虽然是隔离的,但是想象成在一个组内。
①先确定增量数组。根据增量数组可以得到趟数。(增量的大小==分组的个数)
②每一趟都有对应的分组,对于每个组来说,认为每个组的第一个元素有序,所以外层循环开始位置==增量。
③内层循环从最右边已排好序元素开始到0结束,对于每个组来说,无论是与已排好序的元素比较,还是插入元素,移动的距离都=gap。
void Shell(int arr[],int len,int gap)
{
for(int i=gap;i=0;j=j-gap) //此时,每个组移动的距离=gap
{
if(temp
每一趟,将待排序的第一个值作为基准,通过一趟排序,可以将待排序数据分为两半,左边的一半小于等于基准值,右边的一半大于基准值,然后对左右两半分别递归进行排序,直到数据全部有序。基准值两边划分好后,自身是有序的,它就在最终排序好所在的位置上。
①划分的时候涉及到左右边界
②每一趟排序,都会获取一个已排好序的数据位置,根据此位置,再次进行左右区间的划分。
③划分:
首先,定义一个临时变量存储基准值(第一个值,左边界)。 ①定义一个栈,将左右边界压入栈中。 ②判断栈是否为空,栈不为空,则取出两个边界,然后调用划分函数(partition),找到已排好序的位置,再次划分左右新的区间,压入栈中。 ③重复执行,直到栈为空。 先将长度为n的序列,通过划分,使其变为n个长度为1的序列,然后合并,合并为n/2个长度为2的有序组,然后接着合并,直到所有数据都合并到同一个组为止。 ①划分:使其变为n个长度为1的序列。 ②合并:将数据不断进行合并,直到数据合并到同一个组为止。 每次确定一个最大值(通过维护大顶堆实现,因为大顶堆的根结点是所有值的最大值),然后与最后一个位置进行交换。 (1)已知父节点的下标求孩子节点的下标(父推子) 公式: 父节点下标 i 孩子节点下标 2i+1 2i+2 (2)已知孩子节点的下标求父节点的下标(子推父) 公式: 孩子节点的下标 i 父节点的下标 (i-1)/2 ①将数组想象成完全二叉树 ②将完全二叉树调整为一个大顶堆 ③将大顶堆的根节点与最后一个节点进行交换,交换完成后,断开这个节点,使其不参与后续的排序。 ④重新调整为大顶堆,直到大顶堆中剩下一个节点。 注意:调整大顶堆 首次调整由内向外:从最后一个非叶子节点作为根节点的分支二叉树开始调整,然后从右到左,从上到下。 交换后重新调整,只需要调整一次,不需要向首次从内向外调整那样。 不同排序算法的时间复杂度、空间复杂度、稳定性如下: O(nlogn) 不稳定
然后,先从右边界(right)按从右向左的顺序出发,如果最右边的值大于临时变量,则 right--;如果小于等于临时变量,则将此值存放在左边left处的位置
接着,从左边界(left)按从左到右的顺序出发,如果左边的值小于等于临时变量,则left++; 如果大于临时变量, 则将此值存放在右边right处的位置
最后,如果left==right,即两个指针相遇,表示此位置即为基准值所在的位置,基准值此时已排序完成。
采用递归的方法,以基准值的左边作为左递归的右边界,以基准值的右边作为右递归的左边界。
递归的前提:leftint Partition(int arr[],int left,int right)
{
//取第一个值为基准值
int temp=arr[left];
while(left
(2)非递归的方式实现
int Partition(int arr[],int left,int right)
{
//取第一个值为基准值
int temp=arr[left];
while(left
七、归并排序
1.思想(分治)
2.代码实现
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.思想
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
九、总结
排序算法
时间复杂度
空间复杂度
稳定性
冒泡排序
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(logn)
不稳定
归并排序 (递归)
O(nlogn)
O(n)
稳定
堆排序
O(nlogn)
O(1)