学习每种算法前都回忆一下这个动态图
注:
- 【2 for】冒泡、选择、插入都是2个for,一个用于控制将排序范围不断缩小,不去影响已经排好序的序列
- 【3 for, temp】希尔排序是3个for,和插入排序相比多了一个控制d的大小的for
- 【2 for 、2 while、3 ptr】归并排序是2个for,一个控制所需排序数组中的元素全部拷贝到辅助数组中,另一个for对数组进行归并操作,还有两个while,将两个待归并子序列中多余长度的部分之间复制到原数组中
- 【2 while、2 ptr】快速排序,写起来其实比归并排序简单
- 注意:归并算法和快速排序算法中都运用了递归的思想,所以在书写时的那个排序的主函数中内容都应该放入一个 if(low<high){} 的条件判断函数体中,不然会发生栈溢出的情况
- 【3 for】堆排序,主函数中两个for:一个 for 是用于建立大根堆, 一个for用于实现排序过程,保证已经排序完的序列范围不再参与排序,并且重新将未排序序列变成大根堆,HeapAdject 中的for 用于调整根节点和孩子节点的位置,并且控制下坠,也就是往下验证更换位置后是否还是大根堆
一、基于比较的排序
1、冒泡排序(两两交换)
(一个一个对比,每次移动一个未排序序列中的最大值到最后)
auto n=nums.size();
int i,j;
for(i=0;i<n;i++){ //用于固定最后的已经冒泡冒上来的数字,通过i的增加逐渐减少j循环中对比交换的过程次数
for(j=0;j<n-i-1;j++){
if(nums[j]<nums[j+1])
swap(nums[j],nums[j+1]);
}
}
2、选择排序(选最小去对调)
每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列
auto n = nums.size();
int i,j,relmax;
for(i=0;i<n-1;i++){
relmax=n-i-1;//这个是易错点
for(j=0;j<n-i-1&&nums[relmax]<nums[j];j++){
relmax=j;
}
swap(nums[n-i-1],nums[relmax]);
}
return nums;
3、插入排序(插纸牌)
序列分两段,已排序和未排序,每次将未排序序列中的第一个值记录下来,然后把已排序序列中的比这个值大的数都往后移一位,再把该值插入到前面空出来的位置中
插入排序的思路:
1、先用一个循环控制前面已经排好序的序列的范围
auto n=nums.size();
int i,j,temp;
for(i=1;i<n;i++){// 1
}
2、将该序列后的一个值保存在value中
auto n=nums.size();
int i,j,temp;
for(i=1;i<n;i++){// 1
temp=nums[i];// 2
}
3、再用一个for循环去把已经排序完成的序列中比 temp 大的数依次往后移一位
auto n=nums.size();
int i,j,temp;
for(i=1;i<n;i++){// 1
temp=nums[i];// 2
for(j = i-1,j >= 0 && temp < nums[j];j--){// 3
nums[j+1]=nums[j];
}
}
4、最后将所需要插入的 temp 中的值插入到 j+1 处
auto n=nums.size();
int i,j,temp;
for(i=1;i<n;i++){// 1
temp=nums[i];// 2
for(j = i-1;j >= 0 && temp < nums[j];j--){// 3 其中将判断条件 temp < nums[j]和j >= 0写在一起就等价于下面注释所写
nums[j+1]=nums[j];
}
nums[j+1]=temp;// 4
}
return nums;
//等价于下面的代码,如果不满足 nums[j]>temp 会直接执行外部循环,而不是一直等到j不在 >=0 的时候结束循环
if(temp < nums[j]){
for(j = i-1,j >= 0;j--){
nums[j+1]=nums[j];
}
}
//之所以采用这种写法是因为插入排序时前面已经排序完成的序列是有序的,一旦一个数 nums[j]<temo ,则更前面的数也一定是小于temp的
//错误写法
auto n=nums.size();
int i,j,temp;
for(i=1;i<n;i++){
temp=nums[i];
for(j=i-1;j>=0;j--){
if(temp<nums[j])//不能将条件判断放里面
nums[j+1]=nums[j];
}
nums[j+1]=temp;
}
return nums;
4、希尔排序(部分有序—>全局有序)
其实说到底就是在原来插入排序的基础上加入了一个控制序列分段的d参数
//本来想写另一种更加好理解的希尔算法程序,但是太过复杂,没必要
auto n=nums.size();
int i,j,temp,d;
for(d=n/2;d>=1;d=d/2){
for(i=0;i<n;i+=d){//该循环用于分割多个子序列,d为多少,就有多少个子序列,特殊情况:当d等于1时,有n个子序列
}
}
以下程序较为简洁,但是理解需要绕一下(有三个for)
auto n=nums.size();
int i,j,temp,d;
for(d=n/2;d>=1;d=d/2){//第一个for用于间距d的增减
for(i=d;i<n;i++){//第二个for用于控制多个子序列之间的切换轮流进行排序
temp=nums[i];
for(j=i-d;j >=0 && temp < nums[j];j-=d){//第三个for用于保证每个子序列中某个需要插入的数能依次与前面的数进行比较大小
nums[d+j]=nums[j];
}
nums[d+j]=temp;
}
}
return nums;
5、归并排序(先分再合)
采用递归的方式将一个序列逐步变成两两比较的状态
auto n=nums.size();
int *B=(int *)malloc(n*sizeof(int));//这步注意指针
//该函数是用来给后面的递归函数调用的
void merge(int A[],int low ,int mid,int high){
int i,j,k,B[];
for(k=low;k<=high;k++){//第一步,先将原数组low和high之内的元素全部复制到新数组B中
B[k]=A[k];
}
for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){//注意此处的k=i保证原数组序列和新归并的序列起点在大的整体数组中的位置一样
if(B[i]<=B[j])
A[k]=B[i++];
else
A[k]=B[j++];
}
//将数组中剩余的元素复制到A数组中
while(i<=mid){A[k++]=B[i++];}
while(j<=high){A[k++]=B[j++];}
}
//mergeSOrt
void mergeSort(int A[],int low,int high){
if(low<high){
int mid=(low+high)/2;
mergeSort(A,low,mid);//左边的序列排序//注意A后面不要加[]
mergeSort(A,mid+1,high);
merge(A,low,mid,high);
}
}
力扣解题版:
class Solution {
private:
vector<int> temp;
public:
void mergeSort(vector<int>& nums,int low ,int high){//该处的vector<int>& nums注意
if(low<high){//这个位置是易错点
int mid=(low+high)/2;
mergeSort(nums,low,mid);
mergeSort(nums,mid+1,high);
int i,j,k;
for(k=low;k<=high;k++){
temp[k]=nums[k];
}
for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
if(temp[i]<=temp[j])
nums[k]=temp[i++];
else
nums[k]=temp[j++];
}
while(i<=mid){nums[k++]=temp[i++];}
while(j<=high){nums[k++]=temp[j++];}
}
}
vector<int> sortArray(vector<int>& nums) {
temp.resize((int)nums.size(), 0);//设置temp的长度,第二个参数的初始值
mergeSort(nums,0,(int)nums.size() - 1);
return nums;
}
};
6、快速排序(选枢纽,分前后序列)
双指针,low和high
int partition(int nums[],int low,int high){
//int pivot=low;//这种写法是错的,后面low所指向的元素很快会被取代
int pivot=nums[low];
while(low<high){
//这步的作用是,当high所指元素比枢纽元素大时,high就--(当时一开始错的就是这一步)
while(low<high&&nums[high]>=pivot) high--;
nums[low]=nums[high];
while(low<high&&nums[low]<pivot) low++;
nums[high]=nums[low];
}
}
nums[low]=pivot;
return low;
}
void QuickSort(int A[],int low,int high){
if(low<high){
int pivot=partition(A,low,high);//和归并排序的区别在于,快排是先划分再递归,但是划分的过程中就已经包含了排序,而归并算法是直接使用 mid=(low+high)/2 的方式进行划分,所以并不需要调用,此处的 partition 等同于前面归并算法中的 mid=(low+high)/2 和merge(A,low,mid,high);的混合体
QuickSort(A,low,pivot);
QuickSort(A,pivot+1,high);
}
}
【重写时发生了两个错误导致栈溢出】力扣题解(正常快排)
class Solution {
public:
//
int partition(vector<int>& nums,int low,int high){
//int pivot=low;//这种写法是错的,后面low所指向的元素很快会被取代
int pivot=nums[low];
while(low<high){
while(low<high&&nums[high]>=pivot) high--;//此处的low<high漏写
nums[low]=nums[high];
while(low<high&&nums[low]<pivot) low++;
nums[high]=nums[low];
}
nums[low]=pivot;
return low;
}
//
void QuickSort(vector<int>& A,int low,int high){
if(low<high){//此处的low<high漏写
int pivot=partition(A,low,high);
QuickSort(A,low,pivot);
QuickSort(A,pivot+1,high);
}
}
//
vector<int> sortArray(vector<int>& nums) {
QuickSort(nums,0,(int)nums.size()-1);
return nums;
}
};
随机选取枢纽的快排
class Solution {
int partition(vector<int>& nums, int l, int r) {
int pivot = nums[r];
int i = l - 1;
for (int j = l; j <= r - 1; ++j) {
if (nums[j] <= pivot) {
i = i + 1;
swap(nums[i], nums[j]);
}
}
swap(nums[i + 1], nums[r]);
return i + 1;
}
int randomized_partition(vector<int>& nums, int l, int r) {
int i = rand() % (r - l + 1) + l; // 随机选一个作为我们的主元
swap(nums[r], nums[i]);
return partition(nums, l, r);
}
void randomized_quicksort(vector<int>& nums, int l, int r) {
if (l < r) {
int pos = randomized_partition(nums, l, r);
randomized_quicksort(nums, l, pos - 1);
randomized_quicksort(nums, pos + 1, r);
}
}
public:
vector<int> sortArray(vector<int>& nums) {
srand((unsigned)time(NULL));
randomized_quicksort(nums, 0, (int)nums.size() - 1);
return nums;
}
};
快速排序的非递归实现
void QuickSortNotR(int* array,int left,int right)
{
assert(array);
stack<int> s;
s.push(left);
s.push(right);//后入的right,所以要先拿right
while(!s.empty)//栈不为空
{
int right = s.top();
s.pop();
int left = s.top();
s.pop();
int index = PartSort(array,left,right);
if((index - 1) > left)//左子序列
{
s.push(left);
s.push(index - 1);
}
if((index + 1) < right)//右子序列
{
s.push(index + 1);
s.push(right);
}
}
}
————————————————
版权声明:本文为CSDN博主「清枫若待佳人醉」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_36528114/article/details/78667034
7、堆排序(属于选择排序的一种)
相关知识:
大根堆:完全二叉树中,根≥左、右
小根堆:完全二叉树中,根≤左、右
排序步骤(这个程序存在一些错误,正确版参考最后)
1、【2 for 】建立大根堆
一个 for 用来控制往上检查每一个非终端节点
另一个 for 用来保证更换后根节点和孩子节点之后不会导致下面的节点再次违反这个关系,所以用于控制往下检查每个非终端节点
void BuildMaxHeap(vector<int>& nums,int n){//建立大根堆
int k;
for(k=n/2;k>=0;k--){//往上依次检查
HeadAdject(nums,k,n);
}
}
void HeadAdject(vector<int>& nums,int k,int n){//k特指我们需要检查的根节点
int i;
int temp=nums[k];
for(i=2*k;i<n;i*=2){
if(nums[i+1]>nums[i] && i+1<n) //易错点,保证i+1也有小于n
i++;
if(temp>=nums[i])//为了保证排序是稳定的,因此要加上等号
break;
else{
nums[k]=nums[i];
k=i;//一旦更换了位置后,就要再一一向下验证大根堆的正确性
//nums[i]=temp; 一开始这个写错位置了,如果要写在这个位置,那么上面第二个 if(nums[k]>=nums[i]) 应改为if(nums[k]>=nums[i])
}
}
nums[k]=temp;//一开始写错了,写成nums[i]=temp;
}
void HeapSort(vector<int>& nums,int n){
BuildMaxHeap(nums,n);//1
for(int i=n-1;i>=0;i--){//2 and 3
swap(nums[0],nums[i]);
HeadAdject(nums,0,i-1);//错误:此处一开始写的是n,没注意到在swap之后,移动到最后的元素就不能让再动了
}
}
2、将序列的第一个和最后一个元素交换,也就是将堆顶结点的值交换到堆底,因为堆顶元素一定是所有元素中最大的,所以我们就把堆顶元素以后最后面以后就不要去动他了,用一个for去限定这个范围,之后将换到堆顶的去的未排序序列末尾元素进行不停的下坠
3、重复2过程进行排序
最终力扣通过的代码版本
共有两个错误点:
1、由于数组序号是从0开始的,所以父节点和子节点的计算公式出错(正确的应为 )
d
a
d
=
(
s
o
n
−
1
)
/
2
dad = (son-1)/2
dad=(son−1)/2
s o n = d a d ∗ 2 + 1 son = dad*2+1 son=dad∗2+1
2、由于 上面程序中的 HeadSort 和 BuildMaxHeap 两个函数调用 HeadAdject和这个函数是对于 end 这个参数的定义不同导致
正确的排序版本
class Solution {
public:
void HeadAdject(vector<int>& nums,int k,int end){
int dad = k;
int son = dad * 2 + 1;
//这个for第一次调用是正常的调整根节点和孩子节点的位置,第二、三......次调用就是为了验证第一次调用是是否使得下面不满足大根堆的要求
for(;son<=end;) {
if (son + 1 <= end && nums[son] < nums[son + 1]) //比较左右孩子
son++;
if (nums[dad] > nums[son])
break;//也可以是return
else {
swap(nums[dad], nums[son]);//发生了swap,则需要对于更换后的孩子节点进行大根堆验证
dad = son;
son = dad * 2 + 1;
}
}
}
//写程序先写主函数,这个函数是思路
void HeapSort(vector<int>& nums,int n){
for(int k=n/2-1/2;k>=0;k--){//建立大根堆,从最后一个根节点往上依次进行 HeapAdject
HeadAdject(nums,k,n-1);
}
for(int i=n-1;i>=1;i--){
swap(nums[0],nums[i]);
//移动完最大的节点后,重新建立大根堆
HeadAdject(nums,0,i-1);//i-1为易错点,因为swap之后,最后一个元素就不能再动了
}
}
vector<int> sortArray(vector<int>& nums) {
int n=(int)nums.size();
HeapSort(nums,n);
return nums;
}
};
再一次写堆排序(有一些小错误)注释中已经标明
class Solution {
public:
void HeapAdject(vector<int>& nums,int dad,int end){
for(int son=2*dad+1;dad<=end && son<=end;){//dad<=end冗余
if(nums[son]<nums[son+1] && son+1<=end){
swap(nums[son],nums[son+1]);//错误,不是swap,而是son++
}
if(nums[dad]<nums[son]&&son<=end){
swap(nums[dad],nums[son]);
}
else
break;
dad=son;
son=2*dad+1;
}
}
void HeapSort(vector<int>& nums,int end){
for(int dad=end/2;dad>=0;dad--){
HeapAdject(nums,dad,end);
}
for(int i=end;i>=1;i--){//
swap(nums[0],nums[i]);
HeapAdject(nums,0,i-1);//
}
}
vector<int> sortArray(vector<int>& nums) {
int n=(int)nums.size();
HeapSort(nums,n-1);
return nums;
}
};
第二次写的可以运行的版本
class Solution {
public:
void HeapAdject(vector<int>& nums,int k,int end){
int dad = k;
int son = dad * 2 + 1;
for(;son<=end;){
// if(nums[son] < nums[son+1] && son+1 <= end)
if(son+1 <= end && nums[son] < nums[son+1])
son++;
if(nums[dad] > nums[son])
break;
else{
swap(nums[dad],nums[son]);
dad=son;
son=2*dad+1;
}
}
}
void HeapSort(vector<int>& nums,int n){
int end=n-1;
for(int dad=end/2;dad>=0;dad--){
HeapAdject(nums,dad,end);
}
for(int i=end;i>=1;i--){//
swap(nums[0],nums[i]);
HeapAdject(nums,0,i-1);//
}
}
vector<int> sortArray(vector<int>& nums) {
int n=(int)nums.size();
HeapSort(nums,n);
return nums;
}
};
二、
8、计数排序
按照类型去刷题
两种类型
- 数据结构
- 算法
从easy开始做