2023届C/C++软件开发工程师校招面试常问知识点复盘Part 5

36、细讲一下智能指针
  • C++ 11中的智能指针主要是为了解决传统的动态内存申请(new)和释放(delete)时候可能发生的各种错误,如忘记释放导致的内存泄漏,重复释放导致的错误
  • 因此在C++11中引入了3个可以更加方便地管理动态内存的智能指针,分别是shared_ptr<T>,unique_ptr<T>,weak_ptr<T>.
  • 这三个智能指针都是模板类,模板参数中指定智能指针管理的内存对象类型
  • 通常智能指针不需要程序员自己释放动态内存,而是引入了引用计数的特性,以shared_ptr为例,每当一个对象被一个新的shared_ptr引用,引用计数自动加1;每当指向该对象的一个智能指针被释放或者生存周期结束,该对象的引用计数自动减1;当引用计数为0时,表示当前对象不再被任何指针引用,shared_ptr在析构时会释放动态内存
  • unique_ptr:独占某对象的智能指针,引用计数最大为1,只能由一个unique_ptr指向同一个对象;不能拷贝构造和赋值构造,但可以移动构造(右值引用),通过转移所有权的方式,构建一个unique_ptr

  • share_ptr:共享某对象的智能指针,允许多个shared_ptr指向同一个动态内存对象.引用计数可以大于1,当引用计数为0时,动态内存对象自动随着shared_ptr的析构进行销毁.

  • weak_ptr:主要是配合shared_ptr工作的,weak_ptr属于弱引用智能指针,他只负责监控某块内存,但是不会影响内存的引用数.weak_ptr不用来操作内存对象,但是可以通过lock函数获得共享智能指针.之后再通过shared_ptr去操作对象


weak_ptr可以解决以下两种情况的问题:

  • shared_ptr的循环引用,对象A和B中存在对象B和对象A的智能指针shared_ptr,这会导致循环引用,引用计数无法为0,导致无法正常析构,造成内存泄漏

  • ②当希望在类的内部,返回this的共享智能指针shared_ptr时,假如类的外部已经有了一个shared_ptr,那么很容易造成多个shared_ptr被一个地址重复构造,这会导致内存对象被重复释放,造成错误;解决方法是将类继承模板类: enable_shared_from_this<T>,然后调用该类中的函数shared_from_this()由此返回一个shared_ptr,而函数shared_from_this()内部正是使用weak_ptrlock函数实现的.

37、用简洁的方式描述一下死锁?

死锁通常是因为线程加锁使用不当,导致线程都被阻塞,并且阻塞情况无法打开,属于无解状态。

死锁我觉得可以分为三种情况

①加了锁,但是忘记解锁:

  • 如果忘记解锁,当线程再次访问该资源时,因为资源已经被锁定,所以就线程就被阻塞了,但因为无法解锁,所以这种情况无解

②重复加锁

  • 加锁之后,第二次加锁时因为一次加的锁还没有打开,导致线程被阻塞在第二次加锁位置,此时的线程来一个阻塞一个

③互锁

  • 最简单的情况,两个线程分别占用了一个共享资源并加了锁,同时他自己占用的资源又恰巧是对方所需要的资源,所以对方线程就被自己加的锁给阻塞了,又因为对方被自己阻塞了,所以自己需要的资源也不会被释放了。双方是一样的情况,因此相互锁定。
38、map相关的方法的含义和用法:
  • map::count(3):返回的是一个数量,是key等于3的元素个数.更官方的讲:返回key等于执行参数的元素个数

  • map::lower_bound(2):返回首个键值不小于指定值的迭代器,大于等于

  • 这不就是大疆面试的时候问我的问题吗,每次添加一个点进入类时,就计算找个点到原点的距离,将该距离作为key,点node指针作为值value; 给定一个距离Dis,查找哪个点的距离大于等于Dis的最近的点?使用函数lower_bound(Dis)就可以了,该函数返回了指向某元素的迭代器,该元素的key值大于等于Dis且最接近Dis的元素.

  • map::upper_bound(5):返回首个键值大于指定参数的迭代器,大于

  • map::equal_range(key):返回的是一个pair<迭代器1, 迭代器2>,满足左闭右开原则,范围之内的元素的键值都等于指定参数key,迭代器1可以由函数lower_bound(key)获得,迭代器2可以由函数upper_bound(key)获得; 一边是大于等于,另一边是大于,并且左闭右开,所以中间的元素就是等于

39、10大排序算法
  • 稳定排序 :冒泡排序、插入排序、归并排序(O(nlogn))、计数排序、桶排序、基数排序

  • 不稳定排序:选择排序、希尔排序(O(nlogn))、快速排序(O(nlogn))、堆排序(O(nlogn))

  • 稳定性:指的是对于键值相等的两个记录在排序前后的相对顺序是否发生改变不会发生改变的就是稳定排序

1.冒泡排序:

平均时间复杂度 O ( n 2 ) O(n^2) O(n2) 属于稳定排序

空间复杂度 O ( 1 ) O(1) O(1),因为从投至尾其实只需要一个额外空间进行交换数据。

算法描述:

假设升序排列

1、选择下标最小的两个元素比较,较大的元素往后放,如果原来左边元素大于右边元素,那么交换位置

2、递增两个下标重复上述的比较操作,将较大的元素放在右边,一轮遍历到底,就找到了最大的元素,且放在了数组末尾

3、重复第1、2步,但是只遍历的倒数第二个元素,此时就找到了第二大的元素

4、如此反复循环最多n-1轮次,即可完成排序

  • 优化方式:设置一个flag变量,表示本轮是否存在交换操作,如果不存在交换操作,表明排序完成,循环将提前结束

2.选择排序:

平均时间复杂度 O ( n 2 ) O(n^2) O(n2) 属于不稳定排序

空间复杂度也是 O ( 1 ) O(1) O(1)

算法描述:

假设升序排列从左到右

1、遍历整个数组,找出最小值,然后与数组首位元素交换,此时数组第一个元素就是最小值

2、从第二个元素开始,遍历整个数组,找到最小值与数组第二个元素交换,此时数组的第二个元素就是最小值

3、如此重复,最多循环n-1轮次,即可完成排序

3.插入排序:

平均时间复杂度 O ( n 2 ) O(n^2) O(n2) 属于稳定排序

空间复杂度 O ( 1 ) O(1) O(1)

算法描述:(思想就是选择未排序序列中的第一个元素,将其依次已排序序列每个元素比较,最终确定该元素在已排序序列中的位置,并将其插入)

假设从左到右升序排列

1、将第一个元素标记为已排序序列

2、从未排序序列中选择一个元素(通常可选第一个),将该元素与已排序序列中的每个元素依次比较,确定当前元素在已排序序列中的位置

​ 2.1 具体操作可以是:从右往左依次比较,如果当前元素小于已排序序列中的当前元素,那么交换位置,否则保持不动结束本轮循环

3、然后再从未排序序列中选择一个元素重复第二步,直到未排序序列中没有元素,结束排序操作

4.归并排序:

1 1 1 1 1 1 1 1 1 1 1 1

2 2 2 2 2 2

4 4 4

8 4

12

平均时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn) 属于稳定排序

时间复杂度分析:首先两个数组的归并操作这个行为的复杂度是 O ( n ) O(n) O(n),然后归并排序是两年归并,直到只剩下一个分区,这个过程是 O ( l o g n ) O(logn) O(logn),因此时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

空间复杂度分析 O ( n ) O(n) O(n) 需要额外的n个空间去存储每次的归并结果

算法描述:

首先描述一下归并操作:假如有两个已经排序的数组,将两个已排序的数组进行合并成一个大的已排序数组,这个操作就是归并,最低时间复杂度为 O ( n ) O(n) O(n)

1、将未排序的原始数组全部分成大小为1 的分区,因此每个分区都可以认为是已排序的

2、分区与分区之间两两归并,或者选择相邻分区直接进行归并

3、小分区逐渐变成大分区,直到只剩下一个分区,归并排序结束

5.快速排序:

平均时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)属于不稳定排序,空间复杂度是 O ( l o g n ) O(logn) O(logn)

时间消耗分析:递归分治是 O ( l o g n ) O(logn) O(logn) 每次进行的排序是 O ( n ) O(n) O(n),因此综合的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)

空间消耗分析:递归分治是 O ( l o g n ) O(logn) O(logn) 每次进行的排序是 O ( 1 ) O(1) O(1),因此综合的空间复杂度是 O ( l o g n ) O(logn) O(logn)

算法描述:(思想就是分治排序 + 递归

假设从左到右升序排列

1、选择一个中间轴元素mid

2、将大于等于mid的元素都放在mid右边

3、将小于mid的元素都放在mid左边

4、递归的重复上述3步,将其应用于子序列,直到子序列的长度为1

/*
 * 快速排序算法
 * 确定中轴  左右依次  分治  递归
 */
void quickSort(int start, int end, vector<int> &nums)
{
  if (start >= end)
    return;

  int mid = nums[start];  // 确定中轴  一般是左边第一个元素  中轴所在的位置就是“坑”的位置
  int l = start, r = end; // 确定左指针和右指针

  // l < r 就表明本轮排序没有结束
  while (l < r)
  {
    while (l < r && nums[r] >= mid) // 因为坑在左边,所以先处理右指针
      r--;

    if (l < r)           // 表明右指针指向的元素小于了mid
      nums[l] = nums[r]; // 把左边的坑填了

    while (l < r && nums[l] <= mid) // 现在坑跑到了右边,所以处理左指针
      l++;                          // 左指针指向的元素小于等于mid,因此不需要修改位置

    if (l < r)           // 表明左指针指向的元素大于mid
      nums[r] = nums[l]; // 赶紧把右边的坑填了

    /* 如果l不小于r了,说明l等于r;因为l和r都是逐1变动的,这时候也说明本轮排序结束了*/
  }

  if (r == l) nums[l] = mid; // 最后的收尾工作,把mid填在中间位置即可

  quickSort(l + 1, end, nums);
  quickSort(start, l - 1, nums);
}

6.希尔排序:

平均时间复杂度 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)属于不稳定排序

算法思想:(将插入排序与分组相结合)

1、先进行分组较大间隔,可以选择数组长度的一半),在每个组内进行插入排序(间隔越大,每组的元素数就越少)

2、然后缩小间隔,再在每个组内进行插入排序(随着间隔的减少,每个组内的元素也越来越多)

3、直到间隔缩小为1,再进行一次最长数组的插入排序,此时排序完成(间隔为1时,其实就是整个数组了)


为什么不直接使用插入排序呢?为什么分组之后的希尔排序就比直接使用插入排序更加高效呢?

1、插入排序在原序列基本有序时,效率非常高!

2、插入排序在序列元素很多的时候效率很低,但是在数目较少的时候效率损失就没那明显,也即插入排序的低效主要发生在数组元素数目非常多的时候

3、综合以上两点,希尔排序的思想就是先进行分组,分的组越多,每个组内的元素越少,此时进行插入排序效率损失最小;然后再对基本有序的较大数组进行排序,同样效率非常高。

希尔排序有种扬长避短的感觉:既然我在数组元素少的时候效率还行,那我就尽量先排序小数组;既然我的元素基本有序时效率非常高接近 O ( n ) O(n) O(n)那我就先经过小数组排序,然后再对基本有序的数组进行插入排序,巧妙地选择了插入排序效率最高的两种情况

7.计数排序:

  • 平均时间复杂度 O ( n + k ) O(n+k) O(n+k)k是元素的范围、n是元素个数,稳定排序
  • 空间复杂度是 O ( k ) O(k) O(k),就是桶的个数

算法思想:(计数,然后再填充原数组,在小范围内的数组排序,效率极高)

1、根据已知元素的范围,确定设定一个数组或者映射map,数组中的每个坑位代表范围中的一个元素的出现次数,在数组中是依次排列的

2、遍历一边数组,将所有元素的出现次数统计下来

3、根据统计数组的值,将所有元素依次填充回原数组即可,每次填充一个,统计数组相应位置的值减一

8.桶排序:

桶排序是计数排序的升级版。

平均时间复杂度 O ( n + k ) O(n+k) O(n+k),k是桶的个数,n是数据规模

空间复杂度 O ( k ) O(k) O(k)

算法思想:

1、确定桶的数量

2、遍历一边数组,将数组元素均匀的放入不同的桶内

3、对每一个桶内的元素单独进行排序,快速排序、归并排序等

4、合并所有桶的元素到原数组即完成排序

最快的时候是数据可以均匀的分配到每一个桶中

最慢的时候是数据仅仅被分到了一个桶中

9.基数排序:

基数排序是桶排序的扩展:平均时间复杂度 O ( n × k ) O(n\times k) O(n×k)k是桶的个数

他的主要思想是将整数的每一位进行分割,依次比较个位、十位、百位、权重越来越高;每次比较后合并组,再比较下一位;直至没有位可以比较;比如3位数比较3轮,n位数比较n轮

算法描述:

1、先看每一个数的个位,根据个位将每个元素划分到0~9这10个桶中

2、按照先入先出的原则,从0~9个桶内依次取出元素合并成大数组

3、这次考虑每个数的十位,根据十位将每个元素划分到0~9这10个桶中,没有十位的按照0算

4、重复第2步,进行合并,然后再考虑更高的位次,直到最高位也比较完了,之后的合并结果就是已排序的数组

10.堆排序:

堆排序的平均时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),属于不稳定排序

主要是借助了堆这种数据结构,堆分为大顶堆和小顶堆,堆本质上是一个完全二叉树,底层用数组实现

  • 如果整棵树的root节点存放在idx = 1的位置,则有如下结论
  • 如果父节点为i,那么左子节点为2*i,右子节点为2*i+1

如果是大顶堆,那么根节点的值要大于等于子节点的值。

如果是小顶堆,那么根节点的值要小于等于子节点的值。

每次取出整个二叉树的根节点,这就是一个最大值(或最小值),然后重新构建成一个堆;

然后再取出二叉树的根节点,之后再重建成一个堆;

如此取根元素,构建大顶堆/小顶堆的过程,二叉树为空为止,排序也就完成了

40、解决hash冲突的方式

1、开放定址法:意思是只要发生hash冲突,就去寻找下一个空的哈希表地址,只要哈希表足够大,就可以找到一个空的位置解决该哈希冲突

2、再哈希函数法(再散列函数法):意思是多设定一些哈希函数,如果发生了哈希冲突,就换一个哈希函数进行映射,但是增加了计算时间

3、链地址法:这是一个用的比较多的方法。STL中常用的hash table底层就是用的链地址法去解决hash冲突的。

其含义是在每一个hash地址中存放一个链表,用链表来处理hash冲突,将所有关键字为同义词的记录存放在链表中

4、公共溢出区法:表示将所有发生hash冲突的记录,都放在一个新的表中,也就是所谓的公共溢出区

​ 在查询时,首先在基础表中查询,如果结果与查询目标一致,则查询结束;如果不一致,则到溢出表中查询。这种方式在hash冲突数据较少的时候,性能很高。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咖啡与乌龙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值