递归的本质是描述问题:描述要干什么,而不是怎么做。另外注意,留意是否能终止。
递归分为两步,递,以树的形式展开,但是问题的规模减小了;归,最后还要能够返回,就是能够终止。
快排是递归和分治的经典例子
通过一趟排序将要排序的数据分割成前后两部分,其中前边部分的所有数据都比后边部分的所有数据都要小,然后再按此方法对这两部分数据分别进行一次分割,这样进行下去直到不能再分,以此达到整个数据变成有序序列。
这个过程用递归来描述如下:
void QuickSort(int array[], int low, int high) //low 和 high是每次分组的数组的前后两个下标 到 low和high相等时 不能再分 说明已经排序好
{
if (low < high)
{
int n = Partition(array, low, high); //n即是通过某种算法,这里也就是Partition 函数,使索引为n的元素左边的元素都小于它 右边的元素都大于它
QuickSort(array, low, n - 1); //经过第一次分组 索引为n的元素刚好在正确的位置 不用再排 然后前后两部分 这里的n-1 和 n+1
QuickSort(array, n + 1, high); //分别对前后部分重复进行这个过程
}
}
这里的快排的描述已经完毕,还有一个问题就是怎么每次分成前后两组的算法Partition() 函数的实现:
具体可以这样做:选择一个枢纽元素,比如每次分组的第一个元素,从分组的后边还是寻找第一个比枢纽元素大的元素,与枢纽交换位置;然后从前开始,寻找第一个比枢纽元素小的元素,与枢纽元素交换位置;如此这般,每次从后找一个比枢纽元素大的元素,与枢纽元素交换位置,然后从前往后寻找一个比枢纽元素小的元素,再与被换到后边的枢纽元素交换(每次交换实际上都是和枢纽元素交换位置),最后枢纽元素便被换到一个前边的都比其小,后边比起大(或等于)的正确位置上。
int Partition(int array[], int low, int high)
{
// 采用子序列的第一个元素为枢纽元素
int pivot = array[low];
while (low < high)
{
// 从后往前在后半部分中寻找第一个小于枢纽元素的元素
while (low < high && array[high] >= pivot)
{
--high;
}
// 将这个比枢纽元素小的元素交换到前半部分
swap(array[low], array[high]);
// 从前往后在前半部分中寻找第一个大于枢纽元素的元素
while (low < high && array[low] <= pivot)
{
++low;
}
// 将这个比枢纽元素大的元素交换到后半部分
swap(array[low], array[high]);
}
// 返回枢纽元素所在的位置
return low;
}
或者如果每次枢纽元素都选择中间的那个: 保证每次都是同枢纽元素交换交换即可,函数的返回值为枢纽元素的索引
int Partition(int array[], int low, int high)
{
// 采用子序列的第一个元素为枢纽元素
int index = (low + high)/2; //这里index用来追踪枢纽元素的索引
int pivot = array[index];
while (low < high)
{
// 从后往前在后半部分中寻找第一个小于枢纽元素的元素
while (index < high && array[high] >= pivot) //这里是index索引的右边
{
--high;
}
// 将这个比枢纽元素小的元素交换到前半部分
swap(array[index], array[high]);
index = high;
// 从前往后在前半部分中寻找第一个大于枢纽元素的元素
while (low < index && array[low] <= pivot) 这里是index的左边
{
++low;
}
// 将这个比枢纽元素大的元素交换到后半部分
swap(array[low], array[index]);
index = low;
}
// 返回枢纽元素所在的位置
return low;
}
归并排序也是一个递归分治
void MergeSort(int a[], int left, int right)
{
if (left < right)
{
int center = (left + right) / 2; //取得中点用来分组
//将原来序列分为两段
MergeSort(a, left, center); //对前边一组进行这种操作
MergeSort(a, center + 1, right); //对后边一组进行这种操作
//前后两组已经排序号,进行合并
Merge(a, left, center, right); //合并的算法
}
}
现在的问题转化为怎么合并两个已经排序好的序列:
void Merge(int a[], int left, int center, int right)
{
int *t = new int[right - left + 1];//存放中间值结果
int i = left; //前边段的起始
int j = center + 1; //后边段的起始
int k = 0; //中间结果的起始
//合并数组,用插入排序,如果左边大就插入左边的数;右边大就插入右边的数
while (i <= center && j <= right)
{
if (a[i] <= a[j])
t[k++] = a[i++];
else
t[k++] = a[j++];
}
//上面的步骤在执行完后,左或右边都有可能剩余若干个元素,把剩下的元素都赋值过去
if (i == center + 1)
{
while (j <= right)
t[k++] = a[j++];
}
else
{
while (i <= center)
t[k++] = a[i++];
}
//把t[]的元素复制回a[]中left到right段
for (i = left, k = 0; i <= right; i++, k++)
a[i] = t[k];
//释放内存
delete[]t;
}
尾递归
一个求和的例子如下,尾递归通过参数来记录上下两次函数调用之间的关系,尾递归f() 的嵌套中, 仍然是f() ,而不是由f() 组成的一个表达式,仅仅在f() 的参数上做文章。
int tail_recursive(int n, int a) //尾递归
{ //返回值也是f()的形式 而不是包含f() 的一个运算表达式
if (0 == n) //递归到最底层 直接return 返回了 不用一层层的返回
return a;
else
return tail_recursive(n - 1, n + a);
}
f(n, a) = f (n-1 , n + a) = f(n - 2, n-1 + n + a).... = f(1, 2 + 3 + ... + n + a) = f(0, 1 + 2 +3+ ... n-1 + n) , 递归到最底层,直接return 得到最外层的值,而不用一层层的返回。
实际上,尾递归是通过一个额外的参数将函数结果保存起来,进入到最内存之后,将这个参数返回即可。
斐波那契数列的尾递归
int feibolaqie_tail(int n, int first, int second)
{
if (n == 1 || n == 2)
return second;
return feibolaqie_tail(n - 1, second, first + second);
}