既然是详解自然合并排序,那就一步一步讲解算法: (‾◡◝)
学习自然合并排序,首先我们要先知道:
自然合并排序和合并排序的区别:
合并排序 :
是将两个(或两个以上)有序表合并成一个新的有序表
自然合并排序:
就是在合并排序时,建立子数组段,通过数组已排序的(自然排序,不重新排序)子数组段。
下面通过步骤讲解自然合并排序到底是怎样操作的:( ﹁ ﹁ ) ~→
自然合并排序步骤:
简单的说,自然分组排序就是:
(1)先检查数组内部有没有已经排好序的子数组段;
将数组划分成一个一个有序的子数组段,
子数组段个数最大为数组n (´ー∀ー`)
即没有已经排好序的,比如要求升序数组,数组是{9,8,7,6,5},那么子数组段就为{9}、{8}、{7}、{6}、{5}
最小为1 (´ー∀ー`)
即已经按照要求有序了,比如{5,6,7,8,9}。
再来一个不极端的例子:
比如:{4,8,3,7,1,5,6,2},自然排好序的子数组段{4,8}、{3,7}、{1,5,6}、{2};
(2)分好子数组段后,从第一个开始将相邻的两个子数组段合并成一个数组。然后重新分段,再合并,直到子数组段变为1
比如子数组段{4,8}、{3,7}、{1,5,6}、{2}
第一次合并:{3,4,7,8}、{1,2,5,6}
第二次合并:{1,2,3,4,5,6,7,8}
结束
好了。这就是大概步骤了,是不是觉得思想很简单,但是实现起来还是没有头绪 ( ̄_, ̄ )
没关系,我们通过代码一点一点讲解
自然合并排序算法讲解:
(一)首先是第一步骤,先将原数组a进行分段,
我们需要新开辟一个数组,用这个数组来记录下每个子数组段的首位置,这样就可以达到分割数组的目的,给数组起名为temp:
for(i=0;i<n-1;i++) //分段
{
if(a[i]>a[i+1])
{
temp[j++]=i+1;
k++;//k是总共有多少段
}
}
temp[j]=n;
因为循环里我是将i+1作为下标放进去,所以退出条件为n-1
用k来表示总共有多少段,作为一会儿判断子段数为1时合并结束的条件
最后多加一个n的目的是作为最后一段结束的位置,后面函数需要用,可以先往下看
(二)用一个while循环,循环退出条件为k>1,也就是子数组段为1的时候退出
i表示temp下标位置,j表示合并次数,应该小于段数k除以2,比如有6段就需要合并3次,3段合并1次,最后一段空出(我之前用i<k-2来表示结束条件,发现奇偶段会出现问题)
int f=k;
while(k>1)
{
for(j=0,i=0;j<k/2;j++,i+=2) //两个相邻的进行合并
{
hebing(temp[i],temp[i+1],temp[i+2]);
f--;
}
newtemp(temp,k);
k=f;
}
f的作用是暂代一下新的k值,因为在重新标记子数组段函数里需要知道原来有多少段,所以k值在没有经过这个函数前不能减小。
for循环里合并子数组段函数传的参数是:第一个子数组段首位置、第二个子数组段首位置、第二个子数组段末位置(也就是第三个子数组段首位置减1,由于我后面函数是统一将下一个子数组段首位置设为前一个子数组段末位置,所以在这里没有减)
(三)合并子数组段函数
合并两个子数组的思想很简单,开辟一个新数组x,x的大小为最后一个数组末位置减去第一个数组首位置
然后将两个数组大小进行比较,按顺序插入
最后判断一下哪个数组有剩余,加入新数组里
再将新数组里已经排好的值赋给原来数组a。
void hebing(int begin,int mid ,int end ) //合并子数组段函数
{
int i=0,j;
int x[end-begin];
int l1=begin,l2=mid; //标记一下
while(l1!=mid && l2!=end)//合并入新数组x
{
if(b[l1]<b[l2])
{
x[i++]=b[l1++];
}
else
{
x[i++]=b[l2++];
}
}
if(l1==mid) //如果l1排完了,该l2
{
while(l2!=end)
{
x[i++]=b[l2++];
}
}
if(l2==end) //如果l2排完了,该l1
{
while(l1!=mid)
{
x[i++]=b[l1++];
}
}
for(i=0,j=begin;i<end-begin;i++) //排好序的新数组值赋给原数组
{
b[j++]=x[i];
}
}
之前的给temp多赋值一个n,在这里就是作为最后一个子数组段的末位置传入的
(四)重新标记子数组段函数
这个函数思想也很简单,因为是相邻的两两合并,所以直接去掉temp里除了0外的偶数下标值
但是因为每次i是加2的,所以容易跳过最后的单数的值,所以在最后判断一下,如果没有给加上,要不然最后一个子数组段没有末位置会出错
void newtemp(int *temp,int f) //重新标记子数组段函数
{
int i,j=0;
for(i=0;i<=f;i+=2) //赋新值,每次加2,跳过已经被合并的数组下标值
{
temp[j++]=temp[i];
}
if(temp[j-1]!=n)
{
temp[j]=n;
}
}
好了,主要核心代码和思想就解释完了,
完整代码:(我写的感觉不太好,大家尽量自己改进啊) ( -’`-; )
#include<iostream>
#include<vector>
using namespace std;
class solution
{
private:
int *b;int n,k=1;
public:
int* ziran(int *a,int s)
{
b=a;n=s;
int temp[n];
temp[0]=0;
int i,j=1;
for(i=0;i<n-1;i++) //分段
{
if(a[i]>a[i+1])
{
temp[j++]=i+1;
k++;//k是总共有多少段
}
}
temp[j]=n;
for(i=0;i<k;i++)//输出子段
{
cout<<temp[i];
}
cout<<endl;
int f=k;
while(k>1)
{
for(j=0,i=0;j<k/2;j++,i+=2) //两个相邻的进行合并
{
hebing(temp[i],temp[i+1],temp[i+2]);
f--;
}
newtemp(temp,k);
k=f;
}
return a;
}
void newtemp(int *temp,int f)
{
int i,j=0;
for(i=0;i<=f;i+=2)
{
temp[j++]=temp[i];
}
if(temp[j-1]!=n)
{
temp[j]=n;
}
}
void hebing(int begin,int mid ,int end )
{
int i=0,j;
int x[end-begin];
int l1=begin,l2=mid;
while(l1!=mid && l2!=end)//合并入新数组x
{
if(b[l1]<b[l2])
{
x[i++]=b[l1++];
}
else
{
x[i++]=b[l2++];
}
}
if(l1==mid) //如果l1排完了,该l2
{
while(l2!=end)
{
x[i++]=b[l2++];
}
}
if(l2==end) //如果l2排完了,该l1
{
while(l1!=mid)
{
x[i++]=b[l1++];
}
}
for(i=0,j=begin;i<end-begin;i++)
{
b[j++]=x[i];
}
}
};
int main()
{
int n,i;
cout<<"数组个数:";
cin>>n;
int a[n];
for(i=0;i<n;i++)
{
cin>>a[i];
}
int *b;
solution k;
b=k.ziran(a,n);
for(i=0;i<n;i++)
{
cout<<b[i];
}
}
后记
可能我指针学的不太好,所以这个题在写的时候有很多段错误的bug,而且不停徘徊在值和数组下标里很晕,太菜了... ┭┮﹏┭┮
自然合并排序在已知数组已经有很多有序的子段时,效率还是非常高的
开始写这个的时候很懵
然后csdn搜了半天,发现就是简单的几句话带过思想,然后直接是完整代码,感觉这样慢慢去研究代码又没有注释啥的很烦 (>﹏<)
所以想写一篇比较详细的,之后也会更一些碰见比较烦的算法题。
︿( ̄︶ ̄)︿