数据结构与算法的八股文自述

1.1 排序算法

冒泡排序:

冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。

插入排序:

首先,我们将数组中的数据分为两个区间,已排序区间未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。

选择排序:

选择排序算法的实现思路有点类似插入排序,也分已排序区间未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

快速排序:

快排的思想是这样的:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为pivot(分区点)

我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。

快速排序最好时间复杂度、平均时间复杂度都是O(nlogn)最坏时间复杂度为O(n^2)。是一种原地、不稳定的排序算法。

归并排序:

归并排序的核心思想还是蛮简单的。如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

归并排序是一个稳定的排序算法,其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)空间复杂度是 O(n)

桶排序:

桶排序,顾名思义,会用到“桶”,核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)

桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。

计数排序:

计数排序其实是桶排序的一种特殊情况。当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。

计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

基数排序:

基数排序要求数据可以划分成高低位,位之间有递进关系。比较两个数,我们只需要比较高位,高位相同的再比较低位。而且每一位的数据范围不能太大,因为基数排序算法需要借助桶排序或者计数排序来完成每一个位的排序工作。

基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进的关系,如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了。

1.2 快排、堆排、以及归并排序的应用场景

**堆排的应用场景:**比如最大/小的元素,topK之类的场景;

**快排的应用场景:**快排的应用比较广泛,差不多各种语言均提供了快排的API;适用于数据杂乱无章的场景,而且越乱,快排的效率就体现的淋漓尽致;

**归并排序:**若要求排序稳定的场景。

1.3 堆排序

如何理解"堆"?

①堆是一个完全二叉树;完全二叉树要求,除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。

完全二叉树比较适合用数组来存储。用数组来存储完全二叉树是非常节省存储空间的。因为我们不需要存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点。

②堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。对于每个节点的值都大于等于子树中每个节点值的堆,我们叫做**“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做“小顶堆”**。

如何实现堆排序?

堆排序的过程大致分解成两个大的 步骤:建堆排序

① 建堆从下往上从上往下(建堆的时间复杂度是O(n))

第一种建堆的实现思路:

第一种思路:从前往后处理数组数据,并且每个数据插入堆中时,都是从下往上堆化

第二种建堆的实现思路:

第二种思路:是从后往前处理数组,并且每个数据都是从上往下堆化。

② 排序 (排序过程的时间复杂度是O(nlogn))

当堆顶元素移除之后,我们把下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。堆化完成之后,我们再取堆顶的元素,放到下标是 n−1 的位置,一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。

因此,堆排序是一种原地的、时间复杂度为O(nlogn)的排序算法,但是,堆排序是一个不稳定的排序算法。它的最好时间复杂度,最坏时间复杂度以及平均时间复杂度都为O(nlogn)

适用场景:

① 优先级队列

假设我们有 100 个小文件,每个文件的大小是 100MB,每个文件中存储的都是有序的字符串。我们希望将这些 100 个小文件合并成一个有序的大文件。这里就会用到优先级队列。

整体思路有点像归并排序中的合并函数。我们从这 100 个文件中,各取第一个字符串,放入数组中,然后比较大小,把最小的那个字符串放入合并后的大文件中,并从数组中删除。

假设,这个最小的字符串来自于 13.txt 这个小文件,我们就再从这个小文件取下一个字符串,放到数组中,重新比较大小,并且选择最小的放入合并后的大文件,将它从数组中删除。依次类推,直到所有的文件中的数据都放入到大文件为止。

② topK问题

如果每次询问前 K 大数据,我们都基于当前的数据重新计算的话,那时间复杂度就是 O(nlogK),n 表示当前的数据的大小。实际上,我们可以一直都维护一个 K 大小的小顶堆,当有数据被添加到集合中时,我们就拿它与堆顶的元素对比。如果比堆顶元素大,我们就把堆顶元素删除,并且将这个元素插入到堆中;如果比堆顶元素小,则不做处理。这样,无论任何时候需要查询当前的前 K 大数据,我们都可以立刻返回给他。

③ 利用堆动态地求中位数

我们需要维护两个堆,一个大顶堆,一个小顶堆大顶堆中存储前半部分数据小顶堆中存储后半部分数据,且小顶堆中的数据都大于大顶堆中的数据

也就是说,如果有 n 个数据,n 是偶数,我们从小到大排序,那前 n/2 个数据存储在大顶堆中,后 n/2 个数据存储在小顶堆中。这样,大顶堆中的堆顶元素就是我们要找的中位数。如果 n 是奇数,情况是类似的,大顶堆就存储 n/2+1 个数据,小顶堆中就存储 n/2 个数据。

在实际开发当中,为什么快速排序比堆排序性能好?

① 堆排序数据访问的方式没有快速排序友好。

对于快速排序来说,数据是顺序访问的。而对于堆排序来说,数据是跳着访问的。比如,在堆排序中,最重要的一个操作是数据的堆化。例如,我们需要对堆顶结点进行堆化,会依次访问数组下标是1,2,4,8的元素,而不是像快排那样,局部顺序访问,所以对CPU缓存是不友好的

② 对于同样的数据,在排序的过程中,堆排序算法的数据交换次数要多于快速排序。

在排序算法中,有两个概念:有序度逆序度。对于基于比较的排序算法来说,整个排序过程就是由两个基本的操作组成,即比较交换(移动)快速排序的数据交换次数不会比逆序度多。

堆排序的第一步是建堆建堆的过程中会打乱数据原有的相对先后顺序,导致原数据的有序度降低。比如,对于一组已经有序的数据来说,经过建堆之后,数据反而变得更无序了。

1.4 AVL 和 红黑树的区别

1.5 常见的二叉树的应用场景

1.6 堆和栈的区别

堆与栈的区别

(一)数据结构中的堆和栈:

**堆(数据结构):**堆可以被看成是一棵树,如:堆排序。

**栈(数据结构):**一种先进后出的数据结构。

(二)程序内存分区中的堆与栈:

**栈:**栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。

**堆:**堆由开发人员分配和释放, 若开发人员不释放,程序结束时由 OS 回收,分配方式类似于链表。

区别:

堆与栈实际上是操作系统对进程占用的内存空间的两种管理方式,主要有如下几种区别:
(1)管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;

(2)空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小 64bits 的 Windows 默认 1MB,64bits 的 Linux 默认 10MB;

(3)生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。

(4)分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放,无需我们手工实现。

(5)分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。

**(6)存放内容不同。**栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。

从以上可以看到,堆和栈相比,由于大量malloc()/free()或new/delete的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆

无论是堆还是栈,在内存使用时都要防止非法越界,越界导致的非法内存访问可能会摧毁程序的堆、栈数据,轻则导致程序运行处于不确定状态,获取不到预期结果,重则导致程序异常崩溃,这些都是我们编程时与内存打交道时应该注意的问题。


所以堆与栈的区别很明显:

1、栈内存存储的是局部变量而堆内存存储的是实体;

2、栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;

3、栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。

1.7 字典树

字典数又称单词查找树,Trie树,是一种树形结构],是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

它有三个性质:

①根节点不包含字符,除根节点外每一个节点都只包含一个字符;

②从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;

③每个节点的所有子节点包含的字符都不相同。

可以参考一下:

Trie树(字典树)的介绍及Java实现

数据结构与算法之字典树

1.8 跳表

这种链表加多级索引的结构,就是跳表

跳表是一种动态数据结构,支持快速地插入、删除、查找操作,时间复杂度都是 O(logn)

跳表的空间复杂度是 O(n)。不过,跳表的实现非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗。虽然跳表的代码实现并不简单,但是作为一种动态数据结构,比起红黑树来说,实现要简单多了。所以很多时候,我们为了代码的简单、易读,比起红黑树,我们更倾向用跳表

不过,跳表也不能完全替代红黑树。因为红黑树比跳表的出现要早一些,很多编程语言中的 Map 类型都是通过红黑树来实现的。我们做业务开发的时候,直接拿来用就可以了,不用费劲自己去实现一个红黑树,但是跳表并没有一个现成的实现,所以在开发中,如果你想使用跳表,必须要自己实现。

1.9 b 树和 b+树的区别

B树 的单个结点存储的元素比B+树多,自然在整个过程中的磁盘I/O交互就会比 B+树 多,增加了性能开销;

② 相对B-tree来说,B+树所有的查询最终都会在叶子结点上,这也是B+树性能稳定的原因的一个体现;

B+树所有的叶子结点都是通过双向链表相连,范围查询非常方便,这也是B+树最明显的优势。

2.0 深度优先搜索 和 广度优先搜索的区别

实现深度优先遍历的关键在于回溯。自后向前追溯曾经访问过的路径,就叫做回溯。要想实现回溯,可以利用栈的先入后出特性,也可以采用递归的方式(因为递归本身就是基于方法调用栈来实现)。

实现广度优先遍历的关键在于重放。其实,回溯与重放是完全相反的过程。把遍历过的顶点按照之前的遍历顺序重新回顾,就叫做重放。同样的,要实现重放也需要额外的存储空间,可以利用==队列==的先入先出特性来实现。

二叉树的前序、中序、后序遍历,本质上可以认为是深度优先遍历(DFS)。

二叉树的层序遍历,本质上可以认为是广度优先遍历(BFS)。

2.1 红黑树中一个结点中存储了哪些数据呢?

数值、左右指针、颜色

2.2 解决hash冲突的方式有哪些?以及影响hash查找性能的因素有哪些?

1)解决hash冲突的方式:

① 开放定址法;② 再散列函数法;③ 拉链法;④ 公共溢出区法。

2)影响hash 查找性能的因素:

① 散列函数是否均匀;②解决hash冲突的方式;③散列表的装填因子。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐