一、分而治之的思想

  • 分而治之方法与软件设计的模块化方法非常相似
  • 分而治之通常不用于解决问题的小实例,而要解决一个问题的大实例。​一般步骤为:
  • ①把一个大实例分为两个或多个更小的实例
  • ②分别解决每个小实例
  • ③把这些小实例的解组合成原始大实例的解

二、实际应用之找出假币


问题描述

  • 一个袋子有16个硬币,其中只有一个是假币,这个假币比其他的真币重量轻(其他所有真币的重量都是相同的),现在要找出这个假币



普通的解决方法

  • 一般的方法就是逐个的进行比较,一旦遇到质量比较轻的就找到假币了
  • 步骤为:
  • ①比较硬币1与硬币2,如果某个硬币比较轻,那么这个轻的硬币就是假币,终止寻找;否则进行下一步
  • ②继续比较硬币2与硬币3,如果某个硬币比较轻,那么这个轻的硬币就是假币,终止寻找;否则进行下一步
  • ......以此类推,直到找到假币终止寻找



分而治之的解决方法

  • 分而治之的思想是把一个问题的大实例分解为两个或更多的小实例,然后进行比较
  • 此处我们将大实例分解为两个小实例,步骤为:
  • ①把16个硬币分为两组A和B,每组8个硬币,计算每组硬币的总质量并进行比较,较轻的那一组肯定含有假币
  • ②将较轻的那一组继续分组,分为A和B,每组4个硬币,然后计算每组硬币的总质量并进行比较,同理,较轻的那一组肯定含有假币
  • ③将较轻的那一组继续分组,分为A和B,每组2个硬币,然后计算每组硬币的总质量并进行比较,同理,较轻的那一组肯定含有假币
  • ④最终只剩下两个硬币,因此不需要再进行分组了,直接比较,较轻的那一个硬币肯定是假币


三、实际应用之金块问题


问题描述

  • 一个老板有一袋金块,每块金块的重量都不同,现在想要找出最重的那个金块与最轻的那个金块



普通的解决方法

  • 普通的解决办法就是逐个比较,找出最重和最轻的金块
  • 步骤一般为:
  • 假设金块的总数为n
  • 先逐个比较每个金块,找出最重的金块
  • 找出最重的金块之后,从剩余的n-1个金块中再找出最轻的金块
  • 比较次数:​因为找出最重的金块的比较次数为n-1次,从剩余的金块中再找出最轻的金块用了n-2次,所以总的比较次数为2n-3

template<typename T>
bool find_max_min(T arr[], int n,int &maxIndex,int &minIndex)
{
if (n <= 0)
return false;

//先找出最大的
maxIndex = 0;
for (int i = 1; i < n; ++i) {
if (arr[maxIndex] < arr[i])
maxIndex = i;
}

//再从剩余的当中找出最小的
minIndex = 0;
for (int j = 1; j < n; j++) {
if (j == maxIndex)
continue;
if (arr[minIndex] > arr[j])
minIndex = j;
}

return true;
}


int main()
{
int arr[] = { 1,4,2,5,1,8,5 };
int maxIndex, minIndex;

if (find_max_min(arr, sizeof(arr) / sizeof(int), maxIndex, minIndex)) {
std::cout << "max:" << arr[maxIndex] << endl;
std::cout << "min:" << arr[minIndex] << endl;
}

return 0;
}

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_归并排序



分而治之的解决方法

  • 当n<=2时,一次比较就足够了
  • 当n>2时,总体步骤如下:
  • ①将金块分为A和B两个部分
  • ②分别找出A和B中最重的和最轻的,设A中最重和最轻的金块分别为Ha和La,A中最重和最轻的金块分别为Hb和Lb(这一步可以使用递归来实现)
  • ③比较Ha与Hb就可以找出最重的,比较La与Lb就可以找出最轻的
  • 演示案例:
  • ①假设n=8,有8块金块
  • ②将8个金块分为​两个部分A和B,各有4个金块
  • ③因为A中有4个金块,我们将其再分为​两个部分A1和A2,每个部分有2个金块
  • 然后通过一次比较可以找出A1中较重的金块Ha1和La1
  • 再通过一次比较可以找出A2中较轻的金块Ha2和La2
  • 然后再通过一次比较Ha1与Ha2找出A中最重的金块Ha,通过一次比较La1与La2找出A中最轻的金块La
  • ④因为B中有4个金块,我们将其再分为​两个部分B1和B2,每个部分有2个金块
  • 然后通过一次比较可以找出B1中较重的金块Hb1和Lb1
  • 再通过一次比较可以找出B2中较轻的金块Hb2和Lb2
  • 然后再通过一次比较Hb1与Hb2找出A中最重的金块Hb,通过一次比较Lb1与Lb2找出A中最轻的金块Lb
  • ⑤最后进行一次比较Ha和Hb​找出最重的金块​,再进行一次比较La和Lb​找出最轻的金块​。步骤结束
  • 可以看出在上面的分而治之中总共需要比较10次

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_子序列_02

  • 设c(n)为所需要的比较次数​。为了方便,假设n是2的幂:
  • 如果是分而治之法:当n=2时,c(n)=1;对于较大的n,c(n)=2c(n/2)+2
  • 如果是逐个比较方法:c(n)=3n/2-2
  • 因此,使用分而治之方法比逐个比较的方法少用了25%的比较次数



分而治之编码实现

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_分而治之_03

  • 如果使用递归,则步骤如下:
  • ①在上图的二叉树中,沿着根至叶子的路径,把一个大实例划分成为若干个大小为1或2的小实例
  • ②在每个大小为2的实例中,比较确定哪一个较重和哪一个较轻。在节点D、E、F、G完成这种比较。大小为1的实例只有一个金块,它既是最轻的也是最重的
  • ③对较轻的金块进行比较以确定哪一个最轻,对较重的金块进行比较以确定安一个最重。对节点A、B、C执行这种比较
  • 如果使用非递归的方法,代码如下:
  • 复杂度分析:​当n为偶数时,在for循环外部又一次比较,内部有3(n/2-1)次比较。总的比较次数为3n/2。当n为奇数时,在for循环外部没有比较,内部有n(n-1)/2次比较。因此,无论n为奇数或偶数,当n>0时,总的比较次数为[3n/2]-2。这是在最早最大值最小值的算法中,比较次数最少的算法

template<typename T>
bool find_max_min(T arr[], int n,int &maxIndex,int &minIndex)
{
//如果数组大小小于等于0,直接退出
if (n <= 0)
return false;
//如果只有一个金块,那么它既是最重的也是最轻的
if (n == 1) {
maxIndex = minIndex = 0;
return true;
}
//如果金块数量大于等于2,开始下面的部分

int s = 0;//用于标识比较的开始起点
if (n % 2 == 1) {
//如果金块数量为奇数,设定最大索引与最小索引为0,然后从s=1索引处开始进行比较
maxIndex = minIndex = 0;
s = 1;
}
else {
//如果金块数量为偶数,从前两个元素中提取出较小元素与较大元素的索引,然后从s=2索引处开始比较
if (arr[0] > arr[1]) {
maxIndex = 0;
minIndex = 1;
}
else {
maxIndex = 1;
minIndex = 0;
}
s = 2;
}

//从s开始进行比较,每两个元素比较一次
for (int index = s; index < n; index += 2) {
if (arr[index] > arr[index +1]) {
if (arr[index] > arr[maxIndex])
maxIndex = index;
if (arr[index + 1] < arr[minIndex])
minIndex = index + 1;
}
else {
if (arr[index] < arr[minIndex])
minIndex = index;
if (arr[index + 1] > arr[maxIndex])
maxIndex = index + 1;
}
}

return true;
}


int main()
{
int arr[] = { 1,4,2,5,0,1,8,3,8 };
int maxIndex, minIndex;

if (find_max_min(arr, sizeof(arr) / sizeof(int), maxIndex, minIndex)) {
std::cout << "max:" << arr[maxIndex] << endl;
std::cout << "min:" << arr[minIndex] << endl;
}

return 0;
}


四、实际应用之矩阵乘法

  • 待续

五、编码案例(残缺棋盘)

  • 待续

六、编码案例(归并排序)

  • 可以将分而治之的思想来设计排序算法,把n个元素按非递减顺序排列
  • 这种排序算法的思想是:
  • 若n为1,则算法终止
  • 否则,将序列划分为k个子序列(k是不小于2的整数),先对每一个子序列排序,然后将有序子序列归并为一个序列


二路划分

  • 假设将n元素的序列​仅仅划分为两个子序列,​称之为​二路划分
  • 一种二路划分的方法:
  • 把前面n-1个元素放到第一个子序列中(称为A),最后一个元素放到第二个子序列中(称为B)。然后对A递归进行排序,然后再将B仅有的一个元素归并插入到A中
  • 这种方法是插入排序的递归形式。该算法的复杂度为O
  • 另一种二路划分的方法:
  • 将关键字最大的元素放入B,剩余元素放入A,然后对A进行递归排序。然后将A和B归并,此时直接将B添加到A的尾部就可以了



分而治之的方法

  • 上述方案是将n个元素划分为两个极不平衡的子序列A和B。A有n-1个元素,而B仅有一个元素
  • 现在我们划分的平衡一点,假设A包含n/k个元素,B包含其余的元素。递归地应用分而治之对A和B进行排序,然后采用一个被称之为归并的过程,将有序子序列A和B归并成一个序列
  • 假设有8个元素,关键字分别为[10、4、6、3、8、2、5、7]。则有:
  • 如果选定k=2:
  • 则可以划分出两个子序列[10、4、6、3]与[8、2、5、7]
  • 将上面两个子序列进行排序,可以得到两个有序子序列[3、4、6、10]、[2、5、7、8]
  • 现在从头元素开始比较,将两个有序子序列归并到一个子序列。元素2与3比较,2被移到归并序列;3与5比较,3被移动到归并序列;4与5比较,4被移到归并序列;5与6比较,以此类推....
  • 如果选定k=4:
  • 则可以划分出两个子序列[10、4]与[6、3、8、2、5、7]
  • 将上面两个子序列进行排序,可以得到两个有序子序列[4、10]、[2、3、5、6、7、8]
  • 根据相似的原理进行归并,便可以得到有序序列
  • 下面是对分而治之排序算法的伪代码描述。​当生成的较小的实例个数为2,且A划分后的子序列具有n/k元素时,元素便可以得到最后结果:

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_子序列_04

  • 下面是相关的证明:

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_归并排序_05



二路归并排序代码实现

  • 上面的伪代码是在k=2时的排序算法,称为归并排序,更准确的说,是二路归并排序
  • 下图是在k=2时的归并排序的C++函数(非完整版,简略版):
  • 用一个数组a存储元素序列E,并用a返回排序后的序列
  • 当序列E被划分为两个子序列时,不必把它们分别复制到A和B中,只需简单地记录它们在序列E中的左右边界
  • 然后将排序后的子序列归并到一个新数组b中,最后再将它们复制回a中

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_归并排序_06

  • 如果仔细考察上面的程序,就会发现,递归只是简单地对序列反复划分,知道序列的长度变为1,这时再进行归并。这个过程用n为2的幂来描述会更好:
  • 长度为1的子序列被归并为长度为2的有序子序列
  • 长度为2的子序列被归并为长度为4的有序子序列
  • 这个过程不断重复,直到归并为一个长度为n的序列
  • 下图是n=8时的归并(和复制)过程

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_子序列_07



二路归并的迭代器算法(直接归并排序)

  • 有很多方面可以改进上面的C++函数。例如消除递归
  • 二路归并排序的一种迭代器算法是这样的:
  • 首先将每两个相邻的大小为1的子序列归并
  • 然后将每两个相邻的大小为2的子序列归并
  • 如此重复,直到只剩下一个有序序列
  • 轮流地将元素从a归并到b,从b归并到a,实际上消除了从b到a的复制过程,算法如下
  • 下面的函数用归并排序对数组元素a[0:n-1]排序

template<typename T>
void mergeSort(T a[], int n)
{
T *b = new T[n];
int segmentSize = 1;

while (segmentSize < n)
{
mergePass(a, b, n, segmentSize); //从a到b的归并
segmentSize += segmentSize;

mergePass(b, a, n, segmentSize); //从b到a的归并
segmentSize += segmentSize;
}

delete[] b;
}

  • 为了完成排序代码,需要函数mergePass,不过这个函数仅用来确定需要归并的子序列的左右边界。实际的归并是由函数merge完成的

//从x到y归并相邻的数据段
template<typename T>
void mergePass(T x[], T y[], int n, int segmentSize)
{
//下一个数据段的起点
int i = 0;

//从x到y归并相邻的数据段
while (i <= n - 2 * segmentSize)
{
merge(x, y, i, i + segmentSize - 1, i + 2 * segmentSize - 1);
i = i + 2 * segmentSize;
}

//少于两个满数据段
if (i + segmentSize < n)
//剩余两个数据段
merge(x, y, i, i + segmentSize - 1, n - 1);
else
//只剩一个数据段,复制到y
for (int j = i; j < n; j++)
y[j] = x[j];
}

//把两个相邻数据段从c归并到d
template<typename T>
void merge(T c[], T d[], int startOfFirst, int endOfFirst, int endOfSecond)
{
int first = startOfFirst; //第一个数据段的索引
int second = endOfFirst + 1; //第二个数据段的索引
int result = startOfFirst; //归并数据段的索引

//直到有一个数据段归并到归并段d
while ((first <= endOfFirst) && (second <= endOfSecond))
{
if (c[first] <= c[second])
d[result++] = c[first++];
else
d[result++] = c[second++];
}

//归并剩余元素
if (first > endOfFirst)
for (int q = second; q <= endOfSecond; q++)
d[result++] = c[q];
else
for (int q = first; q <= endOfFirst; q++)
d[result++] = c[q];
}

  • 下面验证一下结果:

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_数据段_08



自然归并排序

  • 在自然归并排序中,首先认定在输入序列中已经存在的有序段
  • 例如:
  • 在输入数列[4、8、3、7、1、5、6、2]中可以认定4个有序段:[4、8],[3、7],[1、5、6],[2]
  • 从左至右扫描序列元素,若位置i的元素比位置i+1的元素大,则位置i便是一个分割点。然后归并这些有序段,直到剩下一个有序段
  • 归并有序段1和2可得有序段[3、4、7、8],归并有序段3和4可得有序段[1、2、5、6]。最后归并这两个有序段可得到[1、2、3、4、5、6、7、8]。这样,只需要两次归并
  • 自然归并的最好情况是:​输入序列已经有序。自然归并排序只认定了一个有序段,不需要归并,但上面的mergeSort()函数仍要进行[C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_数据段_09]次归并,因此自然归并排序所需时间为Θ(n),而上面的mergeSort()函数需要用时Θ(nC++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_数据段_10)
  • 自然归并的最坏情况是:​输入序列按递减顺序排序。最初认定的有序段有n个。这是的归并排序和自然归并排序需要相同的归并次数,但自然归并排序为记录有序段的边界需要更多的时间。因此,在最坏情况下的性能,自然归并排序不如直接归并排序
  • 在一般情况下,n个元素序列有n/2个有序段,因为第i个元素关键字大于第i+1个元素关键字的概率是0.5。如果开始的有序段仅有n/2个,那么自然归并排序所需的归并比直接归并排序的要少。但是自然归并排序在认定初始有序段和记录有序段的边界时需要额外时间。​因此,只有输入序列确实有很少的有序段时,才建议使用自然归并排序


七、编码案例(快速排序)

  • 基本思想为:
  • 把n个元素分为三段:​左端left、中间段middle、右端right
  • 中间段仅有一个元素,左端的元素都不大于中间段的元素,右端的元素都不小于中间段的元素。因此可以对left段和right段独立排序,排序之后不用归并,直接就是有序的了
  • middle的元素​称为“支点”或“分割元素”
  • 下面是快速排序的简单描述:

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_归并排序_11


演示说明

  • 设有一个序列[4、8、3、7、1、5、6、2]:
  • 假设以元素6作为支点(middle),则left段包含4、3、1、5、2,right段包含8、7
  • 将left排序的结果为1、2、3、4、5,将right排序的结果为7、8
  • 最终得到有序序列[1、2、3、4、5、6、7、8]
  • 设有一个序列[4、3、1、5、2]:
  • 假设以元素3作为支点(middle),则left段包含1、2,right段包含4、5
  • 将left排序的结果为1、2,将right排序的结果为4、5
  • 最终得到有序序列[1、2、3、4、5]



C++代码实现

  • 下面是quick()的定义:
  • quickSort把数组的​最大元素移动到数组的最右端,​然后调用递归函数_qucikSort执行排序

template<typename T>
int indexOfMax(T a[], int n)
{
int max = 0;
int index = 0;
for (int index = 1; index < n; index++){
if (a[index] > a[max])
max = index;
}
return max;
}

template<typename T>
void quick(T a[], int n)
{
if (n <= 1)
return;

//把最大元素移动到数组的最右端
int max = indexOfMax<T>(a, n);
std::swap(a[max], a[n - 1]);

//然后调用_quickSort进行递归排序
_quickSort(a, 0, n - 2);
}

  • quick()函数中为什么要将最大元素放置于最右边的原因:​下面的_qucikSort()要求每一个数据段,​或者其最大元素位于右端,或者其后继元素大于数据段的所有元素​,因此,把最大元素移到最右端;如果这个条件不满足,例如,当支点是最大元素时,第一个do循环语句的结果是左索引值leftCursor将大于n-1
  • 下面是_quickSort()的定义:
  • _qucikSort​把数据段划分为左、中、右​。​支点(pivot)总是待排序数段的左元素​。其实还可以选择性能更好的排序算法。在后面我们将讨论这种算法
  • 在do-while语句中,把关系操作符<和>改为<=和>=,程序依然正确(这时,数据段最右边的元素比支点要大)

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_归并排序_12

template<typename T>
void _quickSort(T arr[], int leftEnd, int rightEnd)
{
if (leftEnd >= rightEnd)
return;

int leftCursor = leftEnd; // 记录该区间最左索引
int rightCursor = rightEnd; // 记录该区间最右索引
int pivot = arr[leftEnd]; // 我们以区间最左边的值为基准(哨兵)

// 遍历整个区间
while (leftCursor < rightCursor)
{
// 从右向左找出一个比哨兵小的值, 其索引为rightCurosr
while (leftCursor < rightCursor && arr[rightCursor] >= pivot)
rightCursor--;
// 然后将这个比哨兵小的值放到区间左边, 之后将leftCursor++
// 备注, 此处不需要担心arr[leftCursor]的值是否找不到了, 因为我们已经用pivot将其记录下来了
if(leftCursor < rightCursor)
arr[leftCursor++] = arr[rightCursor];

// 从左向右找出一个比哨兵大的值, 其索引为leftCursor
while (leftCursor < rightCursor && arr[leftCursor] <= pivot)
leftCursor++;
// 然后比这个比哨兵大的值放到右边, 之后将rightCursor
if(leftCursor < rightCursor)
arr[rightCursor--] = arr[leftCursor];

// 这一轮比较完了, 如果中间还有元素没有进行比较, 进行下一轮
}

// 把哨兵的值放到leftCursor所指的位置(中间), 因为此时leftCursor是
arr[leftCursor] = pivot;

_quickSort(arr, leftEnd, rightCursor - 1); // 对左区间递归排序
_quickSort(arr, rightCursor + 1, rightEnd); // 对右区间递归排序
}

  • 下面进行测试

int main()
{
int a[] = { 4,5,1,4,2,3,8 };
quick<int>(a, sizeof(a) / sizeof(int));

for (auto val : a) {
std::cout << val << " ";
}

return 0;
}

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_数据段_13



复杂度分析

  • 上面的quickSort()所需的递归栈空间为O(n)。若使用栈来模拟递归,则需要的空间可以减少为O(logn)。在模拟过程中,首先对数据段left和right中较小者进行排序,把较大者的边界放入栈中
  • 在最坏情况下,例如数据段left总是空,这时的快速排序用时为Θ(C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_数据段_14)。在最好情况下,即数据段left和right的元素数目总是大致相同,这时的快速排序用时为O(nlogn)。而快速排序的平均复杂度也是Θ(nlogn)

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_分而治之_15

  • 下图对本专栏的排序算法在平均情况下和最坏情况下的复杂度做了比较:

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_子序列_16



三值取中快速排序

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_归并排序_17



性能测量

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_归并排序_18



C++排序方法

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_分而治之_19


八、编码案例(选择问题)


问题描述

  • 选择问题就是:​从n个元素的数组中找出第k小的元素
  • 选择问题的一个实际应用就是寻找“中值”元素​。此时k=[n/2]。例如查找中间工资、中间年龄等等



解决方法①

  • 选择问题可在O(nlogn)时间内解决。​一种方法是:
  • 首先对n个元素的数组a进行排序(堆排序或归并排序等)
  • 然后取出a[k-1]的元素
  • 使用快速排序可以获得更好的平均性能。尽管该算法在最坏情况下的渐进复杂度比较差,仅为O(C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_数据段_14)
  • 代码如下:

template<typename T>
T select(T a[], int n, int k)
{
if (n < 1 || k < 1 || k > n)
throw std::out_of_range("out_of_range");

//排序,排序之后a为1 2 3 4 4 5 8
std::sort(a, a + n);
return a[k - 1];
}

int main()
{
int a[] = { 4,5,1,4,2,3,8 };
int ret_val = select<int>(a, sizeof(a) / sizeof(int), 5);
if (ret_val)
std::cout << "select success:" << ret_val << std::endl;
else
std::cout << "select failed:" << ret_val << std::endl;

return 0;
}

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_分而治之_21



解决方法②

  • 修改上面的快速排序函数quickSort(),我们可以得到选择问题的一个较快的求解方法
  • 原理为:
  • 如果在两个while循环之后,将支点元素a[leftEnd]交换到a[j],那么a[leftEnd]便是a[leftEnd:rightEnd]中第j-leftEnd+1小的元素
  • 如果要寻找的第k小的元素在a[leftEnd:rightEnd]中:
  • 如果j-leftEnd+1等于k,那么答案就是a[leftEnd]
  • 如果j-leftEnd+1<k,那么要寻找的元素时right中的第k-j+leftEnd-1小的元素,否则是left中第k小的元素
  • 因此,只需进行0次或1次递归调用
  • 代码如下:​在select中的递归调用可用for或while循环来代替

template<typename T>
int indexOfMax(T a[], int n)
{
int max = 0;
int index = 0;
for (int index = 1; index < n; index++) {
if (a[index] > a[max])
max = index;
}
return max;
}

template<typename T>
T select(T a[], int n, int k)
{
if (k<1 || k>n)
throw std::out_of_range("out_of_range");

//把最大元素移动到数组的最右端
int max = indexOfMax<T>(a, n);
std::swap(a[max], a[n - 1]);

//然后调用_quickSort进行递归排序
return _select(a, 0, n - 1, k);
}

template<typename T>
T _select(T a[], int leftEnd, int rightEnd, int k)
{
if (leftEnd >= rightEnd)
return a[leftEnd];

int leftCursor = leftEnd, rightCursor = rightEnd + 1;
T pivot = a[leftEnd];

//将位于左侧不小于支点的元素和位于右侧不大于支点的元素交换
while (true)
{
do { //寻找左侧不小于支点的元素
leftCursor++;
} while (a[leftCursor] < pivot);
do { //寻找右侧不大于支点的元素
rightCursor--;
} while (a[rightCursor] > pivot);

//没有找到交换的元素对,退出
if (leftCursor >= rightCursor)
break;
std::swap(a[leftCursor], a[rightCursor]);
}

if (rightCursor - leftEnd + 1 == k)
return pivot;

//放置支点
a[leftEnd] = a[rightCursor];
a[rightCursor] = pivot;

//第一个数据段递归调用
if (rightCursor - leftEnd + 1 < k)
return _select(a, rightCursor + 1, rightEnd, k - rightCursor + leftEnd - 1);
else
return _select(a, leftEnd, rightCursor - 1, k);
}

int main()
{
int a[] = { 4,5,1,4,2,3,8 };
std::cout << "select success:" << select<int>(a, sizeof(a) / sizeof(int), 5) << std::endl;

return 0;
}

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_归并排序_22



复杂度分析

  • 解决方法②所用的程序在最坏情况下的复杂度是Θ(C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_数据段_14)。所谓最坏情况就是left总是为空,第k小元素总是位于right。如果left和right总是一样大小或大小相差不超过一个元素,那么对上面的程序来说,可得到以下递归表达式:

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_归并排序_24C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_子序列_25C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_子序列_26


九、编码案例(相距最近的点对)

  • 待续

十、分而治之解递归方程


解递归方程的方法

  • 解递归方程有若干技术:替代法、归纳法、特征根法和生成函数法
  • 本节描述一种查表法,可以用来求解许多与分而治之算法有关的递归方程



相关等式

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_数据段_27


十一、复杂度的下限

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_子序列_28


最小最大问题的下限

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_数据段_29



排序算法的下限

C++(数据结构与算法):60---分而治之、分而治之应用(残缺棋盘、归并排序、快速排序_归并排序_30