c语言讨论寻找多数元素(主元素问题)
摘要
c语言讨论寻找多数元素(主元素问题),这个问题最初由投票问题引出,比如在投票系统中如果有一个人的票数超过50%,那么这个人立即当选。
下面我们将问题抽象出来。
一.问题描述
令A[1.....n]是一个整数序列,A中的整数a如果在A中出现的次数多于[n/2]次,那么a就称为多数元素。
比如现在有一个序列,已知其中有一个数出现的次数超过了50%,请你找出这个数。如3,3 ,1 ,1,3 ,2,3中,出现次数超过50%的数是3。
小伙伴们,你们有什么方法吗?
二.方法思路
我当时就想啊,第一个方法,两两比较呗,分别记录每个数字的出现的次数,2个for循环就解决了,代码如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { int n; int a[101]; int count, max=0; printf("数据个数:\n"); scanf_s("%d", &n); int i, j, k = 0; for (i = 1; i <= n; ++i) //循环存入数据 { scanf_s("%d", &a[i]); } for (i = 1; i <= n; ++i) //遍历每一个数据 { count = 0; for (j = 1; j <= n; ++j) { if (a[i] == a[j] && i != j) //遇到相同的数据,计数器加1 { count++; } } if (count > max) //寻找有相同数据最多的数 { max = count; k = a[i]; } } printf("%d\n",k); return 0; }
这种方法显然时间复杂度为O(n^ 2), 即n的平方。那么我有思考了,这样的处理还能更好吗?当然有,我脑中一机灵(哈哈我觉得可能是彼得一机灵,天哪,我为什么会想到这个),想到了排序,排完序后所有相同的数会就会在一起。然后再从头到尾扫一遍,用一个变量count来统计,并再用一个max来更新count的最大值。这段代码之所以比刚才那2个for循环的方法要好,是因为我们可以使用堆排或快排,这样时间复杂度降为O(n*log n),比刚才的O(n^ 2)要快很多。如果不用堆排或快排,则时间复杂度仍为O(n^2),代码如下:
快排:
#include <stdio.h> #include <stdlib.h> #include <string.h> int a[101], n; //定义全局变量,因为这两个变量在子函数中使用 void quicksort(int left, int right) { int i, j, t, temp; if (left > right) { return; } temp = a[left]; i = left; j = right; while (i != j) { //顺序很重要,要先从右往左找 while (a[j] >= temp && i < j) { j--; } //再从左往右找 while (a[i] <= temp && i < j) { i++; } //交换两个数在数组中的位置 if (i < j)//当i与j没有相遇时 { t = a[i]; a[i] = a[j]; a[j] = t; } } //最后将基准数归位 a[left] = a[i]; a[i] = temp; quicksort(left, i - 1); quicksort(i + 1, right); return; } int main(void) { int count = 1, max = 0; printf("数据个数:\n"); scanf_s("%d", &n); int i, k = 0; for (i = 1; i <= n; ++i) //循环存入数据 { scanf_s("%d", &a[i]); } quicksort(1,n); //快速排序 for (i = 1; i <= n; ++i) //遍历每一个数据 { if (a[i] == a[i + 1]) { count++; } else { if (count > max) //寻找有相同数据最多的数 { max = count; k = a[i]; } count = 1; } } printf("%d\n",k); return 0; }
堆排:
/*对于堆,作向上调整,对内部变量h[101]作调整*/ int* siftup(int n, int h[101]) { int i = n, flag = 0, j; while (i >= 1 && flag == 0) { if (h[i] < h[i / 2] && i >= 1) { j = i / 2; int t; t = a[i]; a[i] = a[j]; a[j] = t; i = j; } else { flag = 1; } } return h; }
上面的代码用于堆排序一个数组,排好后在主函数中给出第一个数,与第二个数比较,如相等count加1,如不相等,与max比较,更新max,最终找出多数元素。
/*对于堆,作向下调整,对内部变量h[101]作调整*/ int* siftdown2(int x, int num, int h[101]) { int n = num, flag = 0; int i, j; j = x; while (j <= n && flag == 0) { if (h[j] > h[2 * j] && 2 * j <= n) { i = 2 * j; } else { i = j; } if (h[i] > h[2 * j + 1] && 2 * j + 1 <= n) { i = 2 * j + 1; } if (i != j) { int t; t = a[i]; a[i] = a[j]; a[j] = t; j = i; } else { flag = 1; } } return h; } int main(void) { int count = 1, max = 0; printf("数据个数:\n"); scanf_s("%d", &n); int i, k = 0; for (i = 1; i <= n; ++i) //循环存入数据 { scanf_s("%d", &a[i]); } siftup(n, a); //堆排序 int c = a[1]; int num = n; for (i = 1; i <= n; ++i) //遍历每一个数据 { a[1] = a[num]; --num; siftdown2(1, num, a); //对于堆,作向下调整,对内部变量h[101]作调整 if (a[1] ==c) //找出下一个较小的数,与前一个比较是否相等 { count++; } else { if (count > max) //寻找有相同数据最多的数 { max = count; k = a[i]; } count = 1; } c = a[1]; } printf("%d\n",k); return 0; }
然后想到这,我再一此灵机一动,如果这个数出现的次数大于50%的话,排序之后应该就是位于n/2的位置的那个数,故如果堆排或快排,直接排完序输出n/2位置的那个数就是这串序列的多数元素,时间复杂度变为O(log n)。
三.深入思考
刚才我们的问题是已知这个序列存在一个数出现的次数大于50%,那如果没有一个数出现的次数大于50%,那我们找到的数就一定是错的,那有人就说了,将找到的数在与序列对比一遍,查看它出现的次数是否超过50%即可,当然可以,那么我们上面所有的代码时间复杂度都要再乘以n,时间过于复杂,那么在一个数列中不知道是否存在一个数,它出现的次数大于50%的前提下,如何找到这个数,或验证它不存在呢?
哈哈,新的问题诞生了!!!
我又开始了一大波思考,,,
这里提供一个思路,我开始是这样想的,可以用一个数组来记录每个数出现的次数,就是桶排序那样,数组中的最大值的下标就是出现次数超过50%的数。时间空间复杂度是O(n+m) ,但是如果数的大小分布跨度很大怎么办,比如1,10000,10000000·····这样就很浪费空间。嗯~,补充一下吧,可以将数据离散化一下,再开一个数组一一对应,让1对应1,2对应10000,3对应10000000·····。
听到这你一定晕了吧,哈哈,这只是一个思路,下面我们来点硬货,小编查阅了大量资料(显得我好爱学习呀!!你们可以当我不存在,啊,我飘了),发现这是著名的“多数元素问题”,也叫“主元素问题”。
四. 终结问题(探索主元素问题)
如果一个序列存在一个数,且它出现的次数大于50%,则这个序列有一个特性:在原序列中去除两个不同的元素后,那么在原序列中出现次数超过了50%的数,在新序列中的出现次数也一定会超过50%。
知道了这个特性问题应该很好解决了吧!!
不!我在探索的过程中发现了另一个问题,我们来看看吧。
例:
序列 1,4,4,7,4,1这个序列显然4是多数元素。
去除1, 4 仍然4是多数元素。
去除1, 7更是。
序列1 ,4,4, 7, 4 ,7,7这个序列显然没有多数元素。
但是如去除1, 4,则序列变为4, 7, 4, 7 ,7,这时 7为多数元素。
故产生了一个新问题,在最终序列遍历完后找到的多数元素不一定是多数元素,但如果序列有多数元素存在,则在最终序列遍历完后找到的多数元素一定为该序列的多数元素,故我们需要一个函数来找序列中是否有多数元素
话不多说上代码,先找这个多数元素:
/*找出多数元素*/ int Candidate(int m) { int c=a[m]; //将新序列第一个数开始向后遍历 int count = 1;//定义计数器 int j; for ( j= m + 1; count > 0 && j < n; ++j) //如果不相等的数等于了相等的数,去除该段已遍历的序列 { if (a[j] == c)//如果与第一个数相等,计数器加1 { count++; } else { count--; //不相等减1 } } if (j == n) //序列遍历完 { return c; } else //如没有,则从接下来新的数开始再往后遍历 { return Candidate(j); } }
就上面所说我们找到这个元素后,要判断这个数是否出现次数大于50%,故我们需要一个函数来判断序列中是否有多数元素,代码如下:
/*判断是否有多数元素*/ int Majority() { int c = Candidate(1); int i, count = 0; for (i = 1; i <= n; ++i) { if (a[i] == c) { count++; } } if (count >= n / 2) //如超过一半,则存在多数元素,且为c这个变量中所存的数 { return c; } else { return 0; } }
这个算法将所有数读了一遍,故时间复杂度最优为O(n) 。下面展示完整代码:
#include <stdio.h> /*寻找多数元素(主元素)问题*/ #include <stdlib.h> /*此代码为最简代码(时间复杂度最简)*/ int a[101], n; //定义全局变量,因为这两个变量在子函数中使用 /*找出多数元素*/ int Candidate(int m) { int c=a[m]; //将新序列第一个数开始向后遍历 int count = 1;//定义计数器 int j; for ( j= m + 1; count > 0 && j < n; ++j) //如果不相等的数等于了相等的数,去除该段已遍历的序列 { if (a[j] == c)//如果与第一个数相等,计数器加1 { count++; } else { count--; //不相等减1 } } if (j == n) //序列遍历完 { return c; } else //如没有,则从接下来新的数开始再往后遍历 { return Candidate(j); } } /*判断是否有多数元素*/ int Majority() { int c = Candidate(1); int i, count = 0; for (i = 1; i <= n; ++i) { if (a[i] == c) { count++; } } if (count > n / 2) //如超过一半,则存在多数元素,且为c这个变量中所存的数 { return c; } else { return 0; } } int main(void) { printf("数据个数:\n"); scanf_s("%d",&n); int i; for (i = 1; i <= n; ++i) { scanf_s("%d",&a[i]); } if (i = Majority() != 0) { printf("%d", i); } else { printf("不存在多数元素\n"); } return 0; }
好了,这就是本次小编为大家带来的关于c语言讨论寻找多数元素(主元素问题)的全部内容啦!感谢您的阅读,分享知识,这个世界还能更好!
参考资料
《C语言程序设计(第5版)》 谭浩强 著 清华大学出版社
《C语言程序设计教程 》 王曙燕 主编 人民邮电出版社
《啊哈!算法》 啊哈磊 著 人民邮电出版社
《数据结构与算法分析C语言描述》 [美] 马克·艾伦·维斯 著 机械工业出版社