一、冒泡排序(沉石排序)
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) | 不稳定 |