● 请你回答一下Array&List, 数组和链表的区别
区别 | 数组 | 链表 |
内存 | 在内存中,数组是一块连续的区域。 | 在内存中可以存在任何地方,不要求连续。 |
数组需要预留空间,在使用前要先申请占内存的大小,可能会浪费内存空间。 | 不指定大小 | |
插入、删除 | 插入数据和删除数据效率低,插入数据时,这个位置后面的数据在内存中都要向后移。删除数据时,这个数据后面的数据都要往前移动。 | 但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。 |
随机访问 | 因为数组是连续的,知道每一个数据的内存地址,可以直接找到给地址的数据。 | 如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。 |
扩展 | 并且不利于扩展,数组定义的空间不够时要重新定义数组。 | 不指定大小,扩展方便。链表大小不用定义,数据随意增删。 |
优点 | 支持随机访问,查找速度快 | 插入删除速度快 内存利用率高,不会浪费内存 大小没有固定,拓展很灵活。 |
缺点 | 插入和删除效率低 可能浪费内存 内存空间要求高,必须有足够的连续内存空间 数组大小固定,不能动态拓展 | 不能随机查找,必须从第一个开始遍历,查找效率低 |
场景 | 如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组。 | 经常插入删除的话 |
● 一个长度为N的整形数组,数组中每个元素的取值范围是[0,n-1],判断该数组否有重复的数,请说一下你的思路并手写代码
返回true——有重复的
bool func(int *array,int n){
if(array==NULL) return false;
int i,temp;
for(i=0;i<n;i++){
if(arr[i]!=i){
if(array[array[i]]==array[i]) return true;
temp=array[i];
array[i]=array[array[i]];
array[array[i]]=temp;
}
}
return false;
}//返回true表示有重复的
快排
int func(vector<int> &num,int l,int r){
int pivot=num[l];
while(l<r){
while(l<r&&num[r]>=pivot) r--;
if(l<r) num[l]=num[r];
while(l<r&&num[l]<=pivot) l++;
if(l<r) num[r]=num[l];
}
num[l]=pivot;
return l;
}
void quicksort(vector<int>&num,int l,int r){
if(l<r){
int mid=func(num,l,r);
quicksort(num,l,mid-1);
quicksort(num,mid+1,r);
}
}
其实不需要排序的,只需要根据mid与k的大小关系就行调整就行
● 请问求第k大的数的方法以及各自的复杂度是怎样的,另外追问一下,当有相同元素时,还可以使用什么不同的方法求第k大的元素
首先使用快速排序算法将数组按照从大到小排序,然后取第k个,其时间复杂度最快为O(nlogn)
(1)堆排序:
#include<stdio.h>
void swap(int *a,int *b){
int temp=*a;
*a=*b;
*b=temp;
}
void adjust(int *arr,int i,int n){
int j=2*i+1;
while(j<n){
if(arr[j+1]>arr[j]&&j+1<n) j++;
if(arr[i]>=arr[j]) return;
swap(&arr[i],&arr[j]);
i=j;
j=2*i+1;
}
}
void makeheap(int *arr,int n){
int i=n/2-1;
for(;i>=0;i--){
adjust(arr,i,n);
}
}
void heapsort(int *arr,int n){
int i=0;
makeheap(arr,n);
for(i=n-1;i>=0;i--){
swap(&arr[i],&arr[0]);
adjust(arr,0,i);
}
}
int main(){
int arr[9]={12,3,435,23,1,32,6,7,7};
int n=9;
heapsort(arr,n);
for(int i=0;i<9;i++) printf("%d\n",arr[i]);
return 0;
}
求第k大的话,只需要排序到第k大的数即可,时间复杂度O(n+klogn)
代码修改为:
void heapsort(int *arr,int n,int k){
int i=0;
makeheap(arr,n);
for(i=n-1;i>=n-k;i--){
swap(&arr[i],&arr[0]);
adjust(arr,0,i);
}
}
int main(){
int a[6]={12,32,4,11,67,7};
int n=6;
int k=3;
heapsort(a,n,k);
printf("%d\n",a[n-k]);
return 0;
}
map:时间复杂度,map查找的时间复杂度O(lgn)
int func(int* a, int n, int k) {
map<int, int> m;
for (int i = 0; i < n; i++) {
if (m.count(a[i])) m[a[i]]++;
else
m[a[i]] = 1;
}
map<int, int>::iterator it=m.end();
it--;
int count = 0;
for (; it != m.begin(); it--) {
count += it->second;
if (count >= k) return it->first;
}
it--;
return it->first;
}
int main() {
int a[10] = { 12,43,65,7,2,3,54,76,2,100 };//2,2,3,7,12,43,54,65,67,100
int n = 10;
int k = 3;
int m=func(a, n, k);
cout << m << endl;
return 0;
}
● 请你来介绍一下各种排序算法及时间复杂度
排序 | 时复(平均) | 时复(最坏) | 时复(最好) | 空间复杂度 | 稳定 |
直接插入排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 稳定 |
堆排序 | O(nlgn) | O(nlgn) | O(nlgn) | O(1) | 不稳定 |
快速排序 | O(nlgn) | O(n^2) | O(nlgn) | O(nlgn) | 不稳定 |
归并排序 | O(nlgn) | O(nlgn) | O(nlgn) | O(n) | 稳定 |
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
选择排序(每次排序最小的元素) | O(n^2) | O(n^2) | O(n^2) | O(1) | 稳定 |
计数排序:适用于一定范围的整数排序。在取值范围不是很大的情况下,它的性能在某些情况甚至快过那些O(nlogn)的排序,例如快速排序、归并排序
● 请问海量数据如何去取最大的k个
1.最小堆法
这是一种局部淘汰法。先读取前K个数,建立一个最小堆。然后将剩余的所有数字依次与最小堆的堆顶进行比较,如果小于或等于堆顶数据,则继续比较下一个;否则,删除堆顶元素,并将新数据插入堆中,重新调整最小堆。当遍历完全部数据后,最小堆中的数据即为最大的K个数。
4.分治法
将全部数据分成N份,前提是每份的数据都可以读到内存中进行处理,找到每份数据中最大的K个数。此时剩下N*K个数据,如果内存不能容纳N*K个数据,则再继续分治处理,分成M份,找出每份数据中最大的K个数,如果M*K个数仍然不能读到内存中,则继续分治处理。直到剩余的数可以读入内存中,那么可以对这些数使用快速排序的变形或者归并排序进行处理。
稳定的排序
直接插入排序,冒泡排序,基数排序,归并排序
第6章 第5节 哈希
● 请你来说一说hash表的实现,包括STL中的哈希桶长度常数
●构造哈希的方法:
直接地址法:数据元素关键字k本身或它的线性函数作为它的哈希地址
平方取中法:这个方法是先取关键字的平方,然后根据可使用空间的大小,选取平方数是中间几位为哈希地址。
除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key % p(p≤m)
● 请你说一说哈希冲突的解决方法
1、开放定址
开放地址法有个非常关键的特征,就是所有输入的元素全部存放在哈希表里,也就是说,位桶的实现是不需要任何的链表来实现的,换句话说,也就是这个哈希表的装载因子不会超过1。它的实现是在插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。所以这种方法又称为再散列法。
①线性探查
dii=1,2,3,…,m-1;这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
②二次探查
di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 );这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
2、链地址
将产生冲突的值以链表的形式连起来
将所有哈希值相同的Key通过链表存储。key按顺序插入到链表中
适合无法确定表长的情况,但是会增加空间消耗(指针需要空间)
3、公共溢出区
建立一个公共溢出区域,把hash冲突的元素都放在该溢出区里。查找时,如果发现hash表中对应桶里存在其他元素,还需要在公共溢出区里再次进行查找。
将哈希表分为基本表和溢出表,凡是与基本表发生冲突的元素都填入到溢出表中
4.再哈希法
一次准备构造多个哈希函数,发生冲突时使用其他的哈希函数,直到不再产出冲突
这种方法增加了计算时间(每次冲突都要重新散列,计算时间增加。)
● 请你说一下哈希表的桶个数为什么是质数,合数有何不妥?
哈希表的桶个数使用质数,可以最大程度减少冲突概率,使哈希后的数据分布的更加均匀。如果使用合数,可能会造成很多数据分布会集中在某些点上,从而影响哈希表效率。
比如:1,3,5,7,9,11,13,15 如果取除以4的余数则都分布在1,3
关键字都分布差值为桶的因子2的的地址上
下面我们假设,p=tq,t为常数,q为某个质数,关键词的差值是q的倍数,那么h(x)-h(y)=q*n,
哈希地址 (h(x)-h(y))%p= q*n%(t*q)=(q*n-t*q*r)=q*(n-tr); t位整除的值。
因此,均散列在差值为q的地址上。