我是靠谱客的博主 无情鸡,最近开发中收集的这篇文章主要介绍数据结构---常见秋招、春招问题汇总(持续更新),觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

  • 数据结构

  • 红黑树了解吗,红黑树的插入、删除

  • 哈希&&hashmap的安全性

  • set、map&& unorderset,unordermap

  • AVL树

  • Hashmap&&插入流程

  • Hashmap结构变换的阈值为什么设置成6 和8

  • 那HashMap是线程安全的吗?

  • Hash函数的设计(hash值的计算原理是什么)

  • 1.7和1.8的区别

  • 为什么hashtable安全而map不安全

  • 哈夫曼树和哈夫曼编码

  • CAS

    红黑树了解吗,红黑树的插入、删除O(logN)

    红黑树其实就是进阶版的AVL树,只是给每个数上了颜色; 它的特点是:1、根节点必须为黑色 2、红节点的子节点必须为黑色(意味着红色不能相连) 3、每条路径上的黑节点个数相同

    插入
    当插入一个节点的时候,把这个节点设置成红色,此时如果分三种情况:
    1、该树为空,直接把该节点作为跟节点,然后变成黑色结束
    2、父节点为黑色直接结束
    3、父节点为红色:此时要看叔叔节点,
    如果叔叔节点为为红色,把祖父节点变成红色把叔叔和父节点变成黑色(如果祖父节点是根节点,再把祖父节点变黑)
    在这里插入图片描述
    如果叔叔节点为黑色或者不存在,对该树进行旋转(规则和AVL树一样)然后把上层节点变黑 ;
    在这里插入图片描述
    删除
    1、删除的节点没有子节点:如果为红直接删除,如果为黑删除完还要进行旋转(规则和AVL树一样)
    2、删除的节点只有一个子节点:直接删除,然后把孩子节点拿上来
    3、删除节点有两个孩子:找后记节点(左树最右,右树最左),把后记节点给根节点,然后把问题转化成删除后记节点(也就是前两个)

哈希 table && 哈希map

哈希表的原理(11):哈希表的本质就是数组,它的原理采用直接对应法,让数和数组下标一一对应,但遇到间隔较大的数就非常浪费空间,所以采用哈希函数进行映射,但当有多个值映射到同一个位置就叫哈希冲突,一般采用开放地址法把元素放到后面的空间;
Hashmap的原理(16) 是在Hsah表上的一种改进,它的底层是数组+链表+红黑树的一种结构,当哈希map经过哈希函数处理后遇到哈希冲突的时候,哈希map是采用拉链法来解决的;不过要注意的是当节点大于8,这个链表会转化成红黑树;来提高一个搜索效率。

哈希函数设计
1、除留余数法(还有直接定值法)
除留余数法的一个经验是,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数
f( key ) = key mod p ( p ≤ m )
2、直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址
3、平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。

HashMap的哈希函数怎么设计的吗?
hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作。(hashCode方法可以看做返回的就是对象存储的位置)
知道设计原理吗?(为什么16位和16位碰撞)
这个也叫扰动函数,这么设计有二点原因:
一定要尽可能降低hash碰撞,越分散越好;
算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;

哈希冲突的解决:
1.闭散列——开放地址法(线性探测、平方探测)
线性探测:遇到冲突的就++位置,找下一个
平方探测:遇到冲突就+1 2 3 。。。的平方知道遇到空位置。
补充:因为哈希表如果太满,最后的插入会大概率的遍历大部分节点,效率会变低,所以引入**负载因子(已存元素/数组大小)**如果负载因子太大(一般选0.75)就重新开空间。负载因子设置的越高冲突的概率越高,效率也越低。
这是一种以空间换时间的做法
2.拉链法

HshMap的插入原理
0、判断数组是否为空,如果为空进行初始化
1、首先计算插入元素的hasp值(val%capacity),来计算数组存放的下标;
2、看数组该下标有没有元素,如果没有直接插入;
3、判断该位置的链表长度是不是大于8,如果没有就创建Node节点加入链表;
4,如果大于8就转成红黑树的树形结构进行插入;

那HashMap是线程安全的吗?
hasptable是安全的;hasmap不安全;
不是,在多线程环境下,1.7 会产生死循环、数据覆盖的问题,1.8 中会有数据覆盖的问题

死循环问题
1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;
l例如:A线程在插入节点a,B线程也在插入b,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环。
数据覆盖问题
当A线程判断index位置为空后正好挂起,B线程开始往index位置的写入节点数据,这时A线程恢复现场,执行赋值操作,就把A线程的数据给覆盖了
1.8采用尾插解决死循环

哈希扩容问题:
1.开2倍大小的新表
2.遍历旧表的数据,重新计算在新表中位置
3.释放旧表

set、map&& unorderset,unordermap

Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构

集合 与 字典 的区别:

共同点:集合、字典 可以储存不重复的值
不同点:集合 是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存

set和map:的底层是通过红黑树实现的(通过模板复用的同一颗红黑树,如果是set红黑树的节点就是key,如果是map红黑树的节点就是pair),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的

在这里插入图片描述
unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的在这里插入图片描述

堆问题

1、搞一个降序适用大堆还是小堆:小堆!
使用小堆:小堆的root每次都能保证是最小的数,把root和最后一个叶子节点交换,然后把数组的长度减1,。对于新的二叉树,root的左右子树依然是小堆,在做一遍(做完)向下调整算法就行,就又能选出最小的。
2、TopK问题:10w人选出打游戏最好的10个人。
建一个10人的小堆(选最好20个,就建20个)— 先建10人的小堆,然后把堆顶和第11个数比较,如果比他大就pop掉堆顶,push11,然后进行一次向下调整。之后遍历完所有的数(注意:这种方法选出的10个没有先后顺序,如有需要再排一下)

3、堆排序
//堆排序的思想(默认大堆):首先要从最后一个元素的父节点开始((n-1-1)/2)进行建堆;等到大顶堆建立好以后再把最大元素和最后的位置交换(相当于剔除),此时堆已不再是大堆,需要从根节点位置进行调整;等调整完又此时堆顶又是最大元素;然后重复

堆的插入、删除元素的时间复杂度都是O(log n);
建堆的时间复杂度是O(n);
堆排序的时间复杂度是O(nlog n);

void Adjustdowm(int* a,int size,int root)
{
int parent = root;
int child = parent * 2 + 1while (child < size){
if (child+1<size && a[child + 1] > a[child])
child++;
if (a[child]>a[parent]){
int tem = a[child];
a[child] = a[parent];
a[parent] = tem;
parent = child;
child = child * 2 + 1;
}
else{
break;
}
}
}
void sort(int* arr, int n)
{//n-2是因为n-1是最后元素下标,n-1-1/2是最后元素下标父节点的下标
for (int i = (n - 2) / 2; i >= 0; i--)//这部分是建一个堆,建堆是从最后一个树的父亲为parent开始,因为向下调整建大堆要保证左右子树都是大堆,所以从该位置开始
{
//从最后一个叶子节点的父亲节点开始向下调整
Adjustdowm(arr,n, i);//这里要传n不能传end,否则第一次进去没修改,Adjus里的size就是数组个数
}
int end = n - 1;
while (end > 0)//从最后一个元素开始调整,调整完a[1]元素,已经排好序了
{//利用向下调整算法每次选出最大的一个
//选出来以后和最后一个交换,然后把最后一个覆盖掉
int tem = arr[end];
arr[end] = arr[0];
arr[0] = tem;
Adjustdowm(arr,end, 0);//每次交换完都从0开始直接调整,这里先Ad在end--,是因为end代表的是size,前面已经n-1过了,因此先Ad已经排除了最后最大的那个元素了
end--;
}
}
int main()
{
//Heap hp;
int arr[] = { 7, 4, 3, 2, 1, 97, 2, 9 };
int n = sizeof(arr) / sizeof(arr[0]);
//HeapCreat(&hp, arr, n);
printf("%dn", n);
sort(arr, n);
for (int i = 0; i < n; i++)
{
printf("%dn", arr[i]);
}
}

AVL树

其产生了目的就是为了解决搜索二叉树在单边情况时的效率低下问题
AVL树的平衡调整
LL—>R、 RR——>L; LR——>LR ; RL——>RL
在这里插入图片描述在这里插入图片描述
如何判断LR的方向?
当新加入一个节点,如果产生不平衡,从新加节点从下往上找第一个不平衡的点(因为新加一个点可能引起多个点不平衡),把这两个点连线。然后从不平衡点开始从上往下的前两个方向就是LR的方向。

例:按AVL树规则插入 16、3、7、11、9、26、18、14、15
在这里插入图片描述

Hshmap

Hashmap的原理(16) 是在Hsah表上的一种改进,它的底层是数组+链表+红黑树的一种结构,当哈希map经过哈希函数处理后遇到哈希冲突的时候,哈希map是采用拉链法来解决的;不过要注意的是:
对于插入:这个阈值为8,意思就是当有9个节点的时候链表就会转化为红黑树,查找性也从 O(n)提高到红黑树的 O(logn)
对于移除,当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点来提高一个搜索的效率;不过hashmap是不安全的。

HshMap的插入流程
0、判断数组是否为空,如果为空进行初始化
1、首先计算插入元素的hash值,来计算数组存放的下标(n-1)&hash;
2、看数组该下标有没有元素,如果没有直接插入;
3、判断该位置的链表长度是不是大于8,如果没有就创建Node节点加入链表;
4,如果大于8就转成红黑树的树形结构进行插入;

设置6和8的原因

1、阈值设置8的原因
因为容器中节点分布在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小。所以选择了8作为阀值
2、那么为什么要一个6一个8;
选择6和8(如果链表小于等于6树还原转为链表,大于等于8转为树),中间有个差值7可以有效防止链表和树频繁转换。假设,插入和删除的阈值都是8,如果一个HashMap链表个数在8左右徘徊,它不停的插入删除元素,结构就会发生频繁的转变降低效率,所以给他中间加一个缓冲值。

那HashMap是线程安全的吗?

hasmap不安全;(那HashMap是线程安全的吗?
在多线程环境下,1.7 会产生死循环的问题,1.8 中会有数据覆盖的问题

死循环问题
1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;
l例如:A线程在插入节点1,2(1由起始指针指向,2由next指针),B线程也在插入节点,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,就有可能把2节点放在1的前面,这样就造成了死循环问题
1.8采用尾插解决死循环:新的元素插入到后面就不会
数据覆盖问题(没问别说)
当A线程判断index位置为空后正好挂起,B线程开始往index位置的写入节点数据,这时A线程恢复现场,执行赋值操作,就把B线程的数据给覆盖了

Hash函数的设计(hash值的计算原理是什么)

哈希函数设计
1、除留余数法(还有直接定值法)
除留余数法的一个经验是,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数
f( key ) = key mod p ( p ≤ m )
2、直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址
3、平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。

HashMap的哈希函数怎么设计的吗?
hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作来得到hash值。最后那(n-1)&hash值来确定插入元素的下标(hashCode方法可以看做返回的就是对象存储的位置)
知道设计原理吗?(为什么16位和16位碰撞)
这个也叫扰动函数,这么设计有二点原因:
一定要尽可能降低hash碰撞,越分散越好;
算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;同时我们将高位参与运算,则索引计算结果就不会仅取决于低位。这样就降低了hash冲突的概率;

1.7和1.8有哪些区别

(1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

(3)在JDK1.7的时候是先扩容后插入的,这样就会导致无论这一次插入是不是发生hash冲突都需要进行扩容,如果这次插入的并没有发生Hash冲突的话,那么就会造成一次无效扩容,但是在1.8的时候是先插入再扩容的,优点其实是因为为了减少这一次无效的扩容,原因就是如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容,但是在1.7的时候就会急造成扩容

(3)JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)

为什么hashtable安全而map不安全

//get它搞成了同步方法,保证了get的安全性
public synchronized V get(Object key) {
……
}
//synchronized,同样
public synchronized V put(K key, V value) {
……
}
//也是搞成了同步方法
public synchronized V remove(Object key) {
……
}

因为它的remove,put,get做成了synchronized(同步方法),保证了Hashtable的线程安全性。
但问题任何一个时刻只能有一个线程可以操纵Hashtable,所以其效率比较低

哈夫曼树

1、每个节点有一个权值,从集合中选出权值最小的两个树(删除)构成一个二叉树,然后把新组成这个树加入集合
WPL = Li*Vi;(节点离根节点的距离(深度)*这个节点的权值)
在这里插入图片描述哈夫曼编码:是在在通信电文中的应用。在电文传输中,我们希望电文的总长尽可能短,因此可以对每个字符设计长度不等的编码,让电文中出现较多的字符采用尽可能短的编码。而采用哈夫曼编码;
二义性问题:保证在数据传输的时候不会有歧义,因此在哈夫曼树中编码的字符是树的叶子,这样的话,字符的编码就不可能是另外一个字符的前缀
规则:左为0 ,右为1;
在这里插入图片描述

CAS

利用CPU的CAS指令来完成的非阻塞算法,解决原子操作。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。

问题:
循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

最后

以上就是无情鸡为你收集整理的数据结构---常见秋招、春招问题汇总(持续更新)的全部内容,希望文章能够帮你解决数据结构---常见秋招、春招问题汇总(持续更新)所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(44)

评论列表共有 0 条评论

立即
投稿
返回
顶部