数据结构与算法是计算机发展的基石,现代计算机的起源是数学,数学的核心是算法,计算机历史上每一次大的变革都离不开算法的推动。纵然“条条大路通罗马”,但好的算法永远比提高硬件设备管用。
- 在排序数组中找出给定数字出现的次数
- 计算两个有序整型数组的交集
- 如何找出数组中重复次数最多的数
- 在On的时间复杂度内找出数组中出现次数超过了一半的数
- 找出数组中唯一的重复元素
- 解题方法
- 引申一没有要求每个数组只访问一次不能用辅助存储空间
- 引申二
在排序数组中,找出给定数字出现的次数
在排序数组中,找出给定数字出现的次数。如,【1,2,2,2,3】中2出现的次数是3次。。。
简单粗暴的方法是从头到尾遍历一遍,用个计数器进行计数,统计2出现的次数。
优化方法:该问题在二分查找的基础上进行改进。设数组array为递增序列,需要查找的元素为findData,为了求解给定数字出现的次数,可以分别寻找findData在array中最先出现的位置和最后出现的位置,通过两者的算术运算即可获得该数字的出现次数。编码的时候,用变量last来存储本次查找到的位置,然后根据情况变换查找方向,就可以分别确定最先出现的位置的下标left和最后出现的位置下标right的值。
#include<iostream>
using namespace std;
int BinarySearch( int *a, int length, int num, bool isLeft )
{
int left = 0;
int right = length - 1 ;
int last = 0;
while( left <= right )
{
int mid = ( left + right ) /2;
if( a[mid] < num )
{
left = mid+1;
}
else if( a[mid] > num )
{
right = mid - 1;
}
else
{
last = mid;
if( isLeft ) right = mid-1;
else left = mid + 1;
}
}
return last>0 ? last : -1;
}
int main()
{
int data[] = { 0,1,2,3,3,3,3,3,3,4,5,6,7,13,19 };
int lower = BinarySearch( data,sizeof(data)/sizeof(data[0]),3,true );
int upper = BinarySearch( data,sizeof(data)/sizeof(data[0]),3,false);
int count = upper - lower + 1;
cout << count << endl;
return 0;
}
计算两个有序整型数组的交集
如,两个含有n个元素的有序(非降序)整型数组a和b(数组a和b中都没有重复元素),求出其共同元素。
a = 0 , 1 , 2, 3, 4
b = 1, 3, 5, 7, 9
交集为{ 1,3 }。
计算数组交集可以采用很多种方法,但数组的相对大小一般会影响算法的效率,所以需要根据两个数组的大小来确定采用的方法。。
(1)对于两个数组长度相当的情况,一般可以采取以下3种方法。
- 采用二路归并来遍历两个数组
设两个数组分别为array1[n1] 和 array2[n2],分别以i、j从头开始遍历两个数组。在遍历过程中,如果当前遍历位置的array1[i]和array[j]相等,则此数为两个数组的交集,记录下来,并继续向后遍历array1和array2。如果array1[i] 大于 array2[j] ,则需继续向后遍历array2. 如果array1[i] 小于 array2[j],则需继续向后遍历array1,直到有一个数组结束遍历即停止。
- 顺序遍历两个数组,将数组元素存放到哈希表中,同时对统计的数组元素进行计数。如果为2,则为两者的交集元素。
- 遍历两个数组中的任意一个数组,将遍历得到的元素存放到哈希表,然后遍历另外一个数组,同时对建立的哈希表进行查询,如果存在,则为交集元素。
(2)对于两个数组长度相差悬殊的情况,如果数组a的长度远远大于数组b的长度,则可以采用下面几种方法。
- 依次遍历长度小的数组,将遍历的得到的数组元素在长数组中进行二分查找。具体而言,设两个指向两个数组末尾元素的指针,取较小的那个数在另一个数组中二分查找,找到,则存在一个交集,并且将该目标数组的指针指向该位置的前一个位置。如果没有找到,同样可以找到一个位置,使得目标数组中在该位置后的数肯定不在另一个数组中存在,直接移动该目标数组的指针指向该位置的前一个位置,再循环找,直到一个数组为空为止。因为两个数组中都可能出现重复的数,因此二分查找时,当找到一个相同的数x时,其下标为i,那么下一个二分查找的下界变为i+1,避免x重复使用。
- 采用与方法一类似的方法,但是每次查找在前一次查找的基础上进行,这样可以大大缩小查找表的长度。
- 采用与方法二类似的方法,但是遍历长度小的数组的方式有所不同,从数组头部和尾部同时开始遍历,这样可以进一步缩小查找表的长度。
如何找出数组中重复次数最多的数
例如,数组 { 1,1,2,2,4,4,4,4,5,5,6,6,6,},元素1出现的次数为两次,元素2出现的次数为2次,元素4出现的次数为4次,元素5出现的次数为两次,元素6出现的次数为3此,问题就是要找出出现重复次数最多的数,所以输出应该为元素4.可以采用如下的两种方法来计算数组中重复次数最多的数。
(1)以空间换时间,定义一个数组int count[MAX],并将其数组元素都初始化为0,然后执行for( int i=0;i<100;i++ ) count[A[i]]++;操作,在count中找最大的数,即为重复次数最多的数。
(1)是一种典型的空间换时间的算法。一般情况下,除非内存空间足够大,否则一般不采用这种方法。
(2)使用map映射表,通过引入map表(map是STL的一个关联容器,它提供一对一的数据处理能力,其中第一个为关键字,每个关键字只能在map中出现一次,第二个称为该关键字的值)来记录每一个元素出现的次数,然后判断次数大小,进而找出重复次数最多的元素。
#include<iostream>
#include<map>
using namespace std;
bool findMostFrequentInArray( int *a, int size, int &val )
{
if(size==0)
return false;
map<int, int> m;
for(int i = 0; i<size; i++ )
{
if( ++m[a[i]]>=m[val] )
val = a[i];
}
return true;
}
int main()
{
int a[] = { 1,2,3,4,4,4,5,5,5,5,6 };
int val = 0;
if( findMostFrequentInArray( a,11,val ) )
cout << val << endl;
return 0;
}
在O(n)的时间复杂度内找出数组中出现次数超过了一半的数
如果本题对时间复杂度没有要求,可以采用很多方法。
- 第一种方法是建立一个二维数组,一维存储数组中的数据,二维存这个数出现的次数,出现次数最多的那个数就是要找的那个数。由于某个数出现的次数超过数组长度的一半,所以二维数组的长度只需要这个数组的一半即可,但是这种方法的时间复杂度和空间复杂度都比较大。
- 第二种方法是先对数组排序,然后取中间元素即可,因为如果某个元素的个数超过一半,那么数组排序后该元素必定占据数组的中间位置。如果出现最多的那个数是最小的,那么1—(n+1)/2都是那个数;如果出现最多的那个数是最大的,那么(n-1)/2-n都是那个数;如果不是最小也不是最大,当这个数由最小慢慢变成最大的数时,会发现中间的那个数的值是不变的,所以中间那个数就是要找的那个数。时间复杂度就是排序用的时间,即最快的排序算法的时间复杂度O(nlogn)。
但由于本题对时间复杂度有要求,以上方法显然都达不到要求,不可取,需要采取非常规方法,利用一些其他技巧来实现。
- 每次去除两个不同的数,剩下的数字中重复出现的数字肯定比其他数字多,将规模缩小化。如果每次删除两个不同的数(不管包不包括最高频数),那么在剩余的数字里,原高频数出现的频率一样超过了50%,不断重复这个过程,最后剩下的将全市同样的数字,即最高频数。此算法避免了排序,时间复杂度只有O(n)。
#include<iostream>
using namespace std;
int findMostApperse( int *num, int len )
{
int candidate = 0;
int count = 0;
for( int i=0; i<len; i++ )
{
if( count==0 )
{
candidate = num[i];
count = 1;
}
else
{
if( candidate == num[i] )
count++;
else
count--;
}
}
return candidate;
}
int main()
{
int arr[] = { 2,1,1,2,3,1,1,1 };
int len = sizeof(arr)/sizeof(arr[0]);
cout << findMostApperse( arr,len ) << endl;
return 0;
}
- Hash 法。首先创建一个hash_map,其中key为数组元素值,value为此数出现的次数。遍历一遍数组,用hash_map统计每个数出现的次数,并用两个值存储目前出现次数最多的数和对应出现的次数,此时的时间复杂度为O(n),空间复杂度为O(n),满足题目要求。
- 使用两个变量A和B,其中变量A存储某个数组中的数,变量B用来计数。开始时将变量B初始化为0,遍历数组:
如果当前数与A不同,则需要分两种情况进行讨论
(1)如果B等于0,则令A等于当前数,令B等于1。
(2)如果B大于0,则令B=B-1。
如果当前数与A相同,则令B=B+1。遍历结束时,A中存储的数就是所要找的数。这个算法的时间复杂度是O(n),空间复杂度是O(1)
#include<iostream>
using namespace std;
int main()
{
int i,A,B;
int a[10] = { 1,2,3,1,2,1,1,6,1,1 };
A = a[5];
B = 0;
for( i=0;i<10; i++ )
{
if( B==0 )
{
A = a[i];
B = 1;
}
else if( A== a[i] )
{
B++;
}
else if( A!=a[i] )
{
B--;
}
}
cout << A <<endl;
return 0;
}
找出数组中唯一的重复元素
数组a[N], 1至N-1这N-1个数存放在a[N]中,其中某个数重复一次,写一个函数,找出被重复的数字。要求每个数组元素只能访问一次,不能用辅助存储空间
解题方法
由于题目要求每个数组元素只能访问一次,不用辅助存储空间,可以从原理上入手,采用数学求和法,因为只有一个数字重复一次,而数又是连续的,根据累加和原理,对数组的所有项求和,然后减去1至N-1的和,即为所求的重复数。
引申一:没有要求每个数组只访问一次,不能用辅助存储空间
如果题目没有要求每个数组元素只能访问一次,不用辅助存储空间,还可以用异或法和位图法来求解。
- 异或法
根据异或法的计算方式,每两个相异的数执行异或运算之后,结果为1;每两个相同的数异或之后,结果为0,所以数组a[N]中的N个数异或结果与1至N-1异或的结果再做异或,得到的值即为所求。
- 位图法
位图法的原理是首先申请一个长度为N-1且均为‘0’组成的字符串,然后从头开始遍历数组a[N],取每个数组元素a[i]的值,将其对应的字符串中的相应位置置1,如果已经置1,那么该数就是重复的数。由于采用的是位图法,所以空间复杂度比较大,为O(n)。
引申二
此题进行一个变形:取值为【1,n-1】含n个元素的整数数组,至少存在一个重复数,即可能存在多个重复数,O(n)时间内找出其中任意一个重复数。例如,array【】= { 1,2,2,4,5,4},则2和4均是重复元素。
- 位图法。使用大小为N位图,记录每个元素是否出现过,一旦遇到一个已经出现过的元素,则直接输出。时间复杂度是O(n),空间复杂度是O(n)
- 数组排序法。首先对数组进行计数排序,然后顺次扫描整个数组,直到遇到一个已出现的元素,直接将之输出。时间复杂度为O(n),空间复杂度为O(n)
- Hash法
以上的两种方案都需要额外的存储空间,能不能不使用额外的存储空间?。。数组元素如果是有符号的int型,则本方法可行。将数组元素作为索引,对于元素array[i],如果array[array[i]]大于0,则设置 array[array[i]] = -array[array[i]];如果array[array[i]]小于0,则array[array[i]]是一个重复数