本次我们介绍一种在字符串处理和大量的算法优化方面都有突出作用的算法——手摇算法(三重反转算法/内存反转算法)
1.何为手摇算法:
我们在初学程序设计语言的时候可能都会接触过一种问题,如何将字符串中的元素反转一下,就是逆序排列一遍
可能我们都会毫不犹豫的写出如下的代码段:
//这里采用模板来举例:
T* p1=&data[left];
T* p2=&data[right];
while(p1<p2)
{
T t=*p1;
*p1=*p2;
*p2=*p1;
p1++;
p2--;
}
我们不要以为手摇算法多么的高级,实际上手摇算法非常的好理解
那么如果我们将问题稍微的变一下,反转字符串中两块元素的位置,该怎么做呢?
可能有的人会说,没问题,我们在开辟一块内存保存一部风的元素块到时候我们再复制两次就可以了
那么如果题目加上限制,我们要求空间复杂度只能是O(1),又该如何解决呢?
这里我们就要引入手摇算法的概念了:
手摇算法的处理方式非常的灵活,我们可以转换大小不均衡的元素块,那么他的原理该怎么解释呢?
举例:A B C D E F,我们要求A B和C D E F转换位置
1.我们先观察
根据上面的逆序交换的手段,我们怎么通过逆序交换的手段来实现位置的转换呢:
A B C D E F
C D E F A B
首先:我们推导一遍就明白了
A B C D E F
B A F E D C
C D E F A B (转换完成)
所以说算法存在三次反转的过程,实如其名:
1.反转前一段
2.反转后一段
2.反转整体
很明显我们的空间复杂度仅仅只是交换操作的t,O(1)实至名归
2.代码实现:
#include"iostream"
#include"cstdio"
#include"cstdlib"
#include"cstring"
#define N 100
using namespace std;
template<typename T>
class triple
{
public:
triple()
{
memset(data,0,sizeof(data));
num=ask=0;
}
void set();
void print();
void heart(int,int);
void reverse(int,int);
int returnnum()
{
return num;
}
private:
T data[N];
int num;
int ask;
};
template<typename T>
void triple<T>::set()
{
cout<<"please input the number of the data!"<<endl;
cin>>num;
cout<<"please input the ask number!"<<endl;
cin>>ask;
cout<<"please input the data!"<<endl;
for(int i=1;i<=num;i++) cin>>data[i];
}
template<typename T>
void triple<T>::print()
{
cout<<"After reverse,the data is now!"<<endl;
for(int i=1;i<=num;i++) cout<<data[i]<<' ';
cout<<endl;
}
template<typename T>
void triple<T>::reverse(int left,int right) //上面写过的逆序操作
{
T* p1;
T* p2;
p1=&data[left];
p2=&data[right];
while(p1<p2)
{
T t=*p1;
*p1=*p2;
*p2=t;
p1++;
p2--;
}
}
template<typename T>
void triple<T>::heart(int left,int right) //三重反转
{
reverse(left,ask);
reverse(ask+1,right);
reverse(left,right);
}
int main()
{
triple<int> my;
my.set();
my.heart(1,my.returnnum());
my.print();
return 0;
}
3.应用(归并排序的空间复杂度优化)
众所周知,归并排序虽然拥有可以匹敌快排的速度,但是致命硬伤是我们牺牲了大量的内存(和待排空间一样大小的内存)来进行了时间效率上的优化,但是在小内存的计算机上我们如何才能实现归并排序,而不导致内存溢出呢
这里我们就可以通过手摇算法加以应用,
但是我们要明确一点,手摇算法的优化是对时间复杂度是有所牺牲的,下面我会给出手摇算法优化后,时间复杂度到底退化了多少。
先要知道,我们优化的是归并排序的插入排序部分,不开辟新的内存从而实现排序(左右各自有序的状态下)
归并排序就不在多余赘述了,如果有所遗忘 ,欢迎参考我的博客
地址在这
在进行讲解之前,我们首先要先明确一点:
在递归回溯的过程中,归并排序的左子段和右子段都是各自有序的,这一点非常重要
3.1元素准备:
1.指针p1,指向左子段的开头
2.指针p2,指向右子段的开头
3.2算法描述:
1.i指针不断向后移动,直到找到第一个比j指向的元素大的元素或者直到和j相遇
2.index指针先代替j指向右端的第一个元素
3.j指针不断向后移动,知道找到第一个比i指向元素大的元素或者直到遇到数组的末尾
4.将i——index-1段和index——j段进行手摇,之后将i移动j-index+1空位,然后继续上述操作(一旦相遇i,j或者j到达末尾,在该操作4结束后直接结束算法)
3.3区间段解释:
1 2 3 4 5 6 7 8 9 10
i j
在上述算法的过程中,i--index段都比index+1--j段大,换言之,index+1--j段(这一小段是有序的)应该整体位于i--index段之前,之后的都很好理解了
附上例题解释图就好理解了:
3.4代码封装:
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
#define N 100
using namespace std;
template<typename T>
class bettermerge
{
public:
bettermerge()
{
memset(data,0,sizeof(data));
num=0;
}
void set();
void print();
void premerge()
{
merge(1,num);
}
void merge(int,int);
void tripleone(int,int,int);
void tripletwo(int,int,int);
void reverse(int left,int right);
private:
T data[N];
int num;
};
template<typename T>
void bettermerge<T>::set()
{
cout<<"please input the number the data!"<<endl;
cin>>num;
cout<<"please input the data!"<<endl;
for(int i=1;i<=num;i++) cin>>data[i];
}
template<typename T>
void bettermerge<T>::print()
{
cout<<"After the sort,the data is now:"<<endl;
for(int i=1;i<=num;i++) cout<<data[i]<<' ';
cout<<endl;
}
template<typename T>
void bettermerge<T>::merge(int left,int right)
{
if(left>=right) return ;
else
{
int mid=(left+right)/2;
merge(left,mid);
merge(mid+1,right);
tripleone(left,mid,right);
}
}
template<typename T>
void bettermerge<T>::tripleone(int left,int mid,int right)
{
int i=left;
int j=mid+1;
int index=j;
while(j<=right&&i<j)
{
while(data[i]<=data[j]&&i<j) //注意这里,如果是 <= 那么优化后的归并排序是稳定的,否则就是不稳定的
{
i++;
}
if(i==j) break;
index=j;
while(data[j]<data[i]&&j<=right) j++; //这里没有上面的影响,仔细想一下就会明白
tripletwo(i,index-1,j-1);
i+=j-index; //j-1-index+1=j-index(后一段的长度),记住小心这里j是比data[i]大的,data[j-1]才是要移动的字段的后端
}
}
template<typename T>
void bettermerge<T>::tripletwo(int left,int mid,int right)
{
reverse(left,mid);
reverse(mid+1,right);
reverse(left,right);
}
template<typename T>
void bettermerge<T>::reverse(int left,int right)
{
T* p1=&data[left];
T* p2=&data[right];
while(p1<p2)
{
T t=*p1;
*p1=*p2;
*p2=t;
p1++;
p2--;
}
}
int main()
{
bettermerge<int> my;
my.set();
my.premerge();
my.print();
return 0;
}
3.5时间复杂度退化计算:
首先我们要明确手摇算法的时间复杂度:
因为手摇算法相当于将数组惊醒了两次遍历,去掉常数项之后,时间复杂度是O(n)
最好的情况:左子段和右子段直接全部交换
很明显此时原地归并的复杂度还是O(n*logn
)
最坏的情况:
一段一段的缓慢前进的情况;此时算法的时间复杂度就是n*n
原地归并的复杂度就是O(n*n*logn)
显然这时候,虽然我们手摇算法将空间复杂度优化到了O(1),但是我们牺牲了相对的时间
综合起来原地归并的时间复杂度在O(n*logn)--O(n*n*logn
)之间,而空间复杂度降到了O(1),是可以满足我们特定的要求的