面试题2---数组中重复的数字详解

1.题目

找出数组中重复的数字。
在一个长度为n的数组里的所有数字都在0~n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果数组长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是重复的数字2或者3。

2.笔者解答

bool duplicate(int numbers[],int length,int *duplication)
{
  int i,j;
  for(i=0;i<length;i++)
   for(j=0;j<length;j++)
   {
      if(numbers[i]==numbers[j]&&i!=j)
      {
         *duplication=numbers[i];
         return true;
      }
   }
   return false;
}

3.提出疑问

  • 1.时间复杂度为O(n2),有没有更简单的实现方法呢?
  • 2.题目中“在一个长度为n的数组里的所有数字都在0~n-1的范围内”,这个条件是不是没用到呢?
  • 3.这程序健壮吗?

4.涉及知识点

(一)快速排序

1.主要思想

  • 在数组中选一个基准数(通常为数组第一个);
  • 将数组中小于基准数的数据移到基准数左边,大于基准数的移动到右边;
  • 对于基准数左、右两边的数组,不断重复以上两个过程,知道每个子集只有一个元素,即为全部有序。
    2.代码实现
{
  QSort(L,1,L->length);
}
void QSort(SqList *L,int low,int high)
{
  int pivot;
  if(low<high)
  {
   pivot=Partition(L,low,high);//将L->r[low..high]一分为二,算出枢纽值pivot
   QSort(L,low,pivot-1);//对低子表递归排序
   QSort(L,pivot+1,high);//对高子表递归排序
  }
}
//交换顺序表L中子表的记录,是枢纽记录到位,并返回其所在位置
//此时在它之前(后)的就均不大(小)与它
int Partition(SqList *L,int low,int high)
{
   int pivotkey;
   pivotkey=L->r[low];//用子表的第一个记录作枢纽记录
   while(low<high)//从表的两端交替向中间扫描
   {
     while(low<high&&L->r[high]>=pivotkey)
         high--;
      swap(L,low,high);//将此枢纽记录小的记录交换到低端
     while(low<high&&L->r[low]<=pivotkey)
         low++;
      swap(L,low,high);//将比枢纽大的记录交换到高端
   }
   return low;//返回枢纽所在位置
}

在最优情况下,快速排序算法的时间复杂度为O(nlog(n))。在最坏的情况下,时间复杂度为O(n2)。

(二)哈希表

1.哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
记录的存储位置=f(关键字)
这里的对应关系f成为散列函数,又称为哈希(Hash函数),采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间成为散列表或哈希表(Hash table)。
哈希表hashtable(key,value)就是把key通过一个固定的算法函数即所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当做数组的下标,将value存储在以该数字为下标的数组空间里。(或者:把任意长度的输入(又叫做预映射,pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输出的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是将任意长度的消息压缩到某一固定的消息摘要的函数。)
而当使用哈希表查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。
2. Hash的应用

  • Hash主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做Hash值。也可以说,Hash就是找到一种数据内容和数据存放地址之间的映射关系。
  • 查找:哈希表,又称散列,是一种更加快捷的查找技术。我们之前的查找,都是这样一种思路:集合中拿出来一个元素,看看是否与我们要找的相等,如果不等,缩小范围,继续查找。而哈希表是完全另外一种思路:当我们知道key值以后,我就可以直接计算出这个元素在集合中的位置,根本不需要一次又一次的查找!
    3.Hash表在海量数据处理中有着广泛应用
    Hash Table的查询速度非常的快,几乎是O(1)的时间复杂度。hash就是找到一种数据内容和数据存放地址之间的映射关系。
    散列法:元素特征转变为数组下标的方法。
    这里可能有一个严重的问题:“如果两个字符串在哈希表中对应的位置相同怎么办?”,毕竟一个数组容量是有限的,这种可能性很大。解决该问题的方法很多,首先想到的就是用“链表”。遇到的很多算法都可以转化成链表来解决,只要在哈希表的每个入口挂一个链表,保存所有对应的字符串就OK了。
    4.Hash表优点和缺点
    优点:不论哈希表中有多少数据,查找、插入、删除(有时包括删除)只需要接近常量的时间即O(1)的时间级。实际上,这只需要几条机器指令。哈希表不仅速度快,编程也相对容易。如果不需要有序遍历数据,并且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。
    缺点:它是基于数组的,数组创建后难于扩展,某些哈希表被基本填满时,性能下降得非常严重,所以程序员必须要清楚表中将要存储多少数据(或者准备好定期把数据转移到更大的哈希表,这是一个费时的过程)。

(三)动态数组

1.前言
数组是一种顺序存储的数据结构,在定义数组时,首先要确定数组的大小。静态数组在编译时就需要确定数组的大小,所以,为了防止内存溢出,我们尽量将数组定义的大一些,但是这样太过浪费内存。
动态数组则不同,它不需要再编译时就确定大小,它的大小在程序运行过程中确定,所以可以根据程序需要而灵活的分配数组的大小,相比静态数组,它更“灵活”、“自由”。但是动态数组需要进行显示的内存释放。
2.动态数组内存分配
动态数组进行内存分配的格式为new T[size],size可以不是常量表达式;如下面的例子所示。

int size=10;//此处的size不是常量表达式。
int* Dynamic_Arr2=new int[size];//未初始化

可以看出,虽然我们分配了一个动态数组,其实返回的是一个T类型的指针,指针指向的是数组的第一个元素,从这个角度来说,动态数组更像是指针。也是由于这种性质,导致了动态数组中size可以取0,即返回一个空指针,即分配一个空动态数组是合法的。
3.动态数组初始化
上面的例子中,Dynamic_Arr2并未进行初始化,若想进行默认初始化,需要在数组后面加上一个小括号。

int* Dynamic_Arr3=new int[size]();//默认的初始化
//此时数组的十个值都为0;

4.动态数组释放
释放动态数组时,使用delete[] arr_name;即在数组名前加上一个中括弧;例如

delete [] Dynamic_Arr4;

释放一个动态数组时,或者说是指向数组的指针时,空括号是必须的。它告诉编译器,指针指向一个数组的第一元素。
delete释放数组时逆序进行的,最后一个元素被最先释放,第一个元素最后一个被释放。
使用动态数组时,一定要记得显示的释放内存,否则很容易出错,比如在一个大的工程中,某一个for循环中或者某个函数中申请了内存却没释放,当函数不断地被调用,这些申请的内存会一直堆积,直到最后退出程序。这很可能造成非常大的麻烦。
5.多维动态数组的内存申请

//----------多维数组内存申请------------
int MAX_NUM=10;
int COL_NUM=5,ROW_NUM=3;
double ***Arr3D=new double **[MAX_NUM];
for(int i=0;i<MAX_NUM;i++)
{
   Arr3D[i]=new double *[ROW_NUM];
   for(int j=0;j<ROW_NUM;j++)
   {
      Arr3D[i][j]=new double[COL_NUM];
   }
}

从上面可以看出,多维数组的申请与多维vector的定义类似,是一层一层的申请内存的,返回的是指向指针数组的指针。
6.多维动态数组内存释放
多维动态数组的释放是从最低维度开始的。先释放掉最低维度的一维数组,然后依次释放内存,直到释放掉最高维度。

for(int i=0;i<MAX_NUM;i++)
{
   for(int j=0;j<ROW_NUM;j++)
   {
     delete[] Arr3D[i][j];
   }
   delete[] Arr3D[i];
}
delete[] Arr3D;

4.就题论题

我们注意到数组中的数字都在0~n-1的范围内。如果这个数组中没有重复数字,那么当数组排序之后数字i将出现在下标为i的位置。由于数组中有重复的数字,有些位置可能存在多个数字,同时有些位置可能没有数字。
现在让我们重排这个数组。从头到尾依次扫描这个数组中的每个数字。当扫描到下标为i的数字是,首先比较这个数字(用m表示)是不是等于i。如果是,则接着扫描下一个数字;如果不是,则再拿它和第m个数字进行比较。如果他和第m个数字相等,就找到了一个重复的数字;如果它和第m个数字不相等,就把第i个数字和第m个数字交换,把m放到属于它的位置。接下来再重复这个比较、交换的过程,直到我们发现一个重复的数字。
以数组{2,3,1,0,2,5,3}为例来分析找到重复数字的步骤。数组的第0个数字(从0开始计数,和数组的下标保持一致)是2,与它的下标不相等,于是把它和下标为2的数字1交换。交换之后的数组是{1,3,2,0,2,5,3}。此时第0个数字是1,仍然与它的下标不相等,继续把它和小标为1的数字3交换,得到数组{3,1,2,0,2,5,3}。接下来继续交换第0个数字3和第3个数字0,得到数组{0,1,2,3,2,5,3}。此时第0个数字的数值为0,接着扫描下一个数字。在接下来的几个数字中,下标为1、2、3的3个数字分别为1、2、3,它们的下标和数值都分别相等,因此不需要执行任何操作。接下来扫描到下标为4的数字2。由于它的数值与它的下标不相等,再比较它和下标为2的数字。注意到此时数组中下标为2的数字也是2,也就是数字2在下标为2和下标为4的两个位置都出现了,因此找到了一个重复的数字。
上述的思路可以用如下代码实现:

bool duplicate(int numbers[],int length,int* duplication)
{
  if(numbers==nullptr||length<=0)
  {
    return false;
  }
  for(int i=0;i<length;i++)
  {
    if(numbers[i]<0||numbers[i]>length-1)
       return false;
  }
  for(int i=0;i<length;++i)
  {
    while(numbers[i]!=i)
    {
      if(numbers[i]==numbers[numbers[i]])
      {
        *duplication=numbers[i];
        return true;
      }
      int temp=numbers[i];
      numbers[i]=numbers[temp];
      numbers[temp]=temp;
    }
  }
  return false;
}

代码中尽管有一个两重循环,但每个数字最多只要交换两次就能找到属于它自己的位置,因此总的时间复杂度为O(n)。另外,所有的操作都是在输入数组上进行的,不需要额外分配内存,因此空间复杂度为O(1)。

扫描二维码关注公众号,回复: 11486856 查看本文章

猜你喜欢

转载自blog.csdn.net/Achenming1314/article/details/105340363
今日推荐