暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

算法思想--分治(上)

二进制人生 2019-09-29
537

分治:分而治之,把一个复杂的问题分解成很多规模较小的子问题,然后解决这些子问题,把解决的子问题合并起来,大问题就解决了。


二分查找是我们最早接触分治思想的例子,在一个有序数组中查找指定数,暴力的做法就是拿指定数和数组里面每个数比较一下,相同的话就返回下标,这个是大问题。可以把有序顺序以中间数为分水岭分成两部分,待查数比中间数小,说明待查数在前面部分,否则,在后面部分,这样就把一个大问题分解成两个小问题,而每个小问题都可以继续分解,直至找到答案或者不能再分解为止。


条件

     当然各个思想都有它的使用领域,所以玩这场分治游戏就要遵守它的游戏规则。

       ①   求解问题确实能够分解成若干个规模较小的子问题,并且这些子问题最后能够实现接近或者是O(1)时间求解。

       ②   各个子问题之间不能有依赖关系,并且这些子问题确实能够通过组合得到整个问题的解。

步骤

     通过上面对分治的说明,可以看到分治分三步走:

       ①  分解: 将问题分解成若干了小问题。

       ②  求解: O(1)的时间解决该子问题。

       ③  合并: 子问题逐一合并构成整个问题的解。

我们来看看运用分分治思想解决的几个经典问题。

Top1--第K问题

输入一个无序数组,找到第K小的数。

这就是经典的第K问题,或者说是Topk问题,比如在1000个考生中找分数排名前K的人。


解题思路:

在本质上和快速排序一样,在数组中随便取一个数作为基准base,一般取第一个数,调整数组,将比base小的数放在base的左边,比base大的数放在base的右边,最后记录base的新下标mid(从0开始)。


如果mid刚好等于k-1,那就达到我们的目的了,因为它就是第k小元素。

一般不会那么巧,但是无形中我们已经把这个问题拆解成了两个小问题。如果mid<k,说明第k小元素就在右边那一拨,相反,则在左边那一拨。


那就继续采取同样的办法将这一拨数据重新分成两拨,直到mid=k为止。如果这样一直分下去直到最后一拨的数据只剩一个,那这就是快速排序了。

#include <sys/time.h>
#include <stdio.h>
 
#define N 20
 
void getData(int d[], int n)
{

    time_t t;
    srand((unsigned) time(&t));  /* 设置随机数起始值 */
  
    int i;
    for(i=0; i < n; i++)
        d[i] = rand() % 100/* 获得0-99之间的整数值 */
}
void result_output(int a[])
{

    int i;
    for (i = 0; i < N; i++)
      printf("%d ", a[i]);
    printf("\n");
}
 
int split(int a[], int low, int high)
{

  int part_element = a[low];
  
  for (;;) {
    while (low < high && part_element <= a[high])
      high--;
    if (low >= high) break;
    a[low++] = a[high];
  
    while (low < high && a[low] <= part_element)
      low++;
    if (low >= high) break;
    a[high--] = a[low];
  }
  
  a[low] = part_element;
  return high;
 
}
 
int select(int a[], int low, int high, int k)
{

  int middle;
  
  for(;;) {
      middle = split(a, low, high);
      if(middle == k)
          return a[k];
      else if(middle < k)
          low 
= middle+1;

      else /* if(middle > k) */
          high = middle-1;
  }
 
}
 
int main(void)
{

  int a[N], k, r;
  
  getData(a, N); /* 获得数据放入数组a中 */
  
  printf("datas: \n");
  result_output(a);
  
  scanf("%d", &k);
  if(k >= 0 && k <= N-1) {
      r = select(a, 0, N - 1, k-1);
      printf("result=%d\n", r);
  } else
      printf("input error: k=%d\n", k)
;

  
  return 0;
}


 

Top2归并排序

归并排序是分治法的一个典型的应用。先使每个子序列有序,再将两个有序子序列合并成一个有序表。


如图所示就是归并排序的基本步骤。首先利用函数递归,将原数组不断分割直至变成不可分割的,只含一个元素的子序列,此时每个子序列都能被看成是有序的子序列。然后进行合并,每一轮合并将逆着拆分的过程逐渐在原数组中形成有序。下面这张图很形象的描述了归并排序的过程。

 



复杂度:

平均、最好、最坏的时间复杂度都为:O(n*log n)。在数据量较大时,归并排序是一种效率很高的方法,使用时需要注意内存空间利用的问题,因为递归次数太多容易造成爆栈;在数据量较小比如数组中只有50个元素要排序时,其速度甚至低于冒泡排序。

void MergeArray(int a[], int start, int mid, int end, int temp[]){
int i = start, j = mid + 1, k = 0;

while(i <= mid && j <= end){ //合并两个数组时,将两个数组的元素逐个比较,

较小的放在临时数组的前面形成有序,一个数组复制结束后就退出

if(a[i] < a[j]) temp[k++] = a[i++];
else temp[k++] = a[j++];
}

while(i <= mid){ //若此时左端数组未复制完,就将左端数组未复制完的部分直接连

在临时数组后面

temp[k++] = a[i++];
}

while(j <= end){ //若此时右端数组未复制完,

就将右端数组未复制完的部分直接连在临时数组后面

temp[k++] = a[j++];
}
for(i = 0; i < k; i++){ //将临时数组中已排序的好的元素复制到原数组对应位置
a[start + i] = temp[i];
}
}

void MergeSort(int a[], int start, int end, int temp[]){
if(start >= end) return; //只有一个元素了,不用再分解
int mid = (start + end) / 2;
MergeSort(a, start, mid, temp); //递归拆分
MergeSort(a, mid+1, end, temp); //递归拆分
MergeArray(a, start, mid, end, temp); //合并
}

int main()
{

int a[20] = {5,32,6,4,8,9,23,16,67,13,65,78,12,15,68,45,21,34,54,78};
int b[20];
MergeSort(a,0,19,b);
for(int i=0;i<20;i++)
printf("%d,",b[i]);
}

Top3快速排序

将第K问题稍作修改,即得到快排。

void split(int a[], int low, int high)
{

int base = a[low];
int tmp = high;

for (;;) {
while (low < high && base <= a[high])
high--;
if (low >= high) break;
a[low++] = a[high];

while (low < high && a[low] < base)
low++;
if (low >= high) break;
a[high--] = a[low];
}
a[low] = base;
if(low-1>0)
split(a,0,low-1);
if(low+1<tmp)
split(a,low+1,tmp);

}

我们顺便来分析下快排的复杂度。因为很多人不理解nlogn是怎么来的。实际上,严谨的写法应该是nlog2n,注意2是底数,这里没写成下标。


快速排序的基本思想是:每次从无序的序列中找出一个数作为中间点(可以把第一个数作为中间点),然后把小于中间点的数放在中间点的左边,把大于中间点的数放在中间点的右边;对以上过程重复log(n)次得到有序的序列。


快速排序的时间复杂性分析:排序的大体如下图所示,假设有1到8代表要排序的数,快速排序会递归log(8)=3次,每次对n个数进行一次处理,所以他的时间复杂度为n*log(n)。所以排序问题的时间复杂度可以认为是对排序数据的总的操作次数。

 

 

文章转载自二进制人生,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论