分治法解主元素问题

分治法

分治法的思想是将一个难以直接解决的大问题划分成一些规模较小的子问题,分别求解各个子问题,再合并子问题的解得到原问题的解。一般来说,分治法的求解过程由以下二个阶段组成:

  1. 划分:把规模为 n 的原问题划分为 k 个规模较小的子问题。
  2. 求解子问题:各子问题的解法与原问题的解法通常是相同的,可以用递归的方法求解各个子问题,有时递归处理也可以用循环来实现。
  3. 把各个子问题的解合并起来,合并的代价因情况不同有很大差异.分治算法的效率很大程度上依赖于合并的实现。

主元素问题

主元素问题是设 T[0:n-1] 是 n 个元素的数组,如果其中某个元素 x 在整个数组中的出现次数超过 n/2,则称 x 为数组 T 的主元素。
主元素和归并排序、最大子段和等情景类似。对于一个规模为 n 的数据集而言,若数据集中有一个元素的出现次数大于 n/2,我们就认为它是主元素。注意到主元素不一定所有的数据集都存在,因此需要对找不到的情况做考虑。
如果对于一个数据集存在主元素,可以将数据集任意分为 2 等分。分别求出这 2 部分的主元素,如果 2 部分的主元素相同,则数据集的主元素就一定等于 2 个子集的主元素。若 2 个子集的主元素不同,则再分别求出这 2 个主元素在整个数据集中的个数,选取其中总个数大于 n/2 的元素为主元素,反之则判定主元素不存在。

伪代码

特殊情况

显然,主元素的存在性是唯一的。考虑最极端的情况,一个数据集中只有 2 个不同的元素,这 2 个不同的元素的个数以 1:1 的关系出现。此时由于主元素的判定条件是元素个数超过 n/2,因此这 2 个元素均不能是主元素。
考虑第二种极端情况,数据集如下所示:“1,2,1,2,1,3,2,3,2,3”。当把这个数据集对半分成2部分时,第一部分的主元素是 1,第二部分的主元素是 3,对于整个数据集而言它们都不是主元素。注意到元素 2 其实是数据集中个数最多的元素,但是它同样也不能满足主元素的定义。从中我们也可以得知,将数据集均等分为 2 部分时,原数据集的主元素一定是 2 个子集的主元素之一。其他元素均不需要考虑,即使其他元素中存在这个数据集的众数,其个数也不可能符合主元素的定义。

代码编写

findMainElement() 函数

int findMainElement(vector<int> dataset, int front, int rear)
{
      int mid = (front + rear) / 2;
      int mainElement1;
      int mainElement2;
      int count1 = 0;
      int count2 = 0;
	
      if(mid == rear)    //递归出口,仅有 1 个元素时 
      {
            return dataset[mid];
      }
	
      mainElement1 = findMainElement(dataset, front, mid);    //对数据集左半边找主元素 
      mainElement2 = findMainElement(dataset, mid + 1, rear);    //对数据集右半边找主元素 
      if(mainElement1 == mainElement2)    //找到的主元素相同 
      {
            return mainElement1;
      }
      else
      {
            num2++;
    	    num3 += (rear - front);
            for(int i = front; i <= rear; i++)    //遍历统计 2 个主元素分别的出现次数 
            {
                  if(dataset[i] == mainElement1)
		          {
                        count1++;
                  }
                  else if(dataset[i] == mainElement2)
		          {
                        count2++;
                  }  
            }
        
            if(count1 > (rear - mid))    //根据次数返回主元素 
            {
                  return mainElement1;	
            }
            else if(count2 > (rear - mid))
            {
                  return mainElement2;
            }    
            else    //无解,返回无穷大 
            {
                  return INT_MAX;	
            } 
      }
}

主函数

int main()
{
      char file_name[20];
      vector<int> dataset;
      vector<int>::iterator it;
      int mainElement;
    
      cin >> file_name;
      dataset = file_Read(file_name);
      mainElement = findMainElement(dataset, 0, dataset.size());
      if (mainElement != INT_MAX)
      {
            cout << "数据集的主元素为:" << mainElement << endl;
      }
      else
      { 
            cout << "该数据集没有主元素" << endl;
      }
      cout << num1 << " " << num2 << " " << num3 << endl; 
      return 0;
}

实验数据

数据集

数据集序号 数据集数据量(个) 数据集特点
1 100 存在主元素,一般情况
2 100 存在主元素,一般情况
3 100 不存在主元素,一般情况
4 100 不存在主元素,一般情况
5 100 存在主元素,元素抱团出现
6 100 不存在主元素,元素抱团出现
7 100 存在主元素,2个元素交替出现
8 100 不存在主元素,2个元素交替出现
9 100 不存在主元素,3个元素交替出现
10 100 (-10000,10000)随机数
11 100 (-10000,10000)随机数

结果数据

实验序号 分治次数 统计元素操作次数 统计元素操作规模
1 201 15 284
2 201 66 208
3 201 74 162
4 201 64 91
5 201 16 174
6 201 46 204
7 201 91 509
8 201 92 532
9 201 79 170
10 201 64 91
11 201 64 91

数据分析

从实验数据我们可以看到,当数据集规模相同时,使用分治法划分的子问题的规模是一样的。但是在合并分治的结果时,如果两个子集返回的主元素不相等,就需要去统计 2 个主元素的出现次数,这时也会带来较大的时间开销。而且使用分治法进行直接分割,没有考虑数据集原本的状态,会使得原本抱团出现的元素被切割成毫无特征的子集,从而使算法的执行次数维持在一定的水平无法降低。同时也能明显地看到,当数据集的分布过于分散时,算法在统计主元素出现次数的操作会有很大的开销。

算法的时间复杂度递推式如下,得出时间复杂度为 O(n) = n㏒n。由于算法存在对元素个数的统计操作,因此算法实际的时间开销 (T(n)) 会比理论分析的复杂度更高。

总结

可以明显地看到分治算法虽然能够解决问题,但是它很可能并不是种聪明的做法。分治法会把数据集分割个多个数据子集进行求解,数据子集往往是相互独立的小问题。可能拿到的数据集本身具有一定的特征,但是分治算法不能提取数据集的特性而是直接划分,导致数据集的特征被破坏,进而导致分治算法的规模会保持在一定的水平难以降低。而且纯分治算法求解主元素问题时,涉及到对元素的个数统计,使得划分的子问题也同样存在很大的开销。此时还不如直接使用快速排序对数据集预处理,然后使用蛮力法统计不同元素的出现次数,虽然时间复杂度没有降低,但是T(n)可以得到优化。总而言之,分治算法的确可以解决一些问题,但是它不一定是这个问题的最优解,还是要具体问题具体分析。

参考资料

《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社
《算法设计与分析(第二版)》——王红梅,胡明 编著,清华大学出版社
C++文件和流

posted @ 2020-11-01 23:01  乌漆WhiteMoon  阅读(1195)  评论(2编辑  收藏  举报