前言
更多整理笔记见:计算机基础-保研笔记分享
数据结构
插入排序
- 直接插入排序(稳定)
- 将序列分为有序部分和无序部分
- 从无序部分依次选择元素与有序部分比较找到合适的位置,将原来的元素进行移动,将元素插入到相应位置上
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 希尔排序(不稳定)
- 先将序列分为若干个子序列(每个子序列都是以 d d d为增量),对各子序列进行直接插入排序
- 等到序列基本有序时,再对整个序列进行一次直接插入排序
- 优点:让关键字值小的元素能够很快移动到前面,且序列基本有序时进行直接插入排序时间效率会提升很多
- 时间复杂度: O ( n 1.3 ) O(n^{1.3}) O(n1.3)到 O ( n 1.5 ) O(n^{1.5}) O(n1.5)之间
交换排序
- 冒泡排序(稳定)
- 每一趟都将相邻元素两两比较,可以按照“前小后大”的规则进行交换
- 总共进行 n − 1 n-1 n−1趟,每趟比较 n − i n-i n−i次
- 优点:每一趟不仅能找到一个最大的元素放到序列后面,而且还把其他元素理顺,并且如果下一趟排序没有发生交换则可以提前结束排序
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 快速排序(不稳定)
- 在序列中任意选择一个元素作为中心(一般选择中间那个元素)
- 比它大的元素一律放后面,比它小的元素一律放前面,形成左右两个子序列
- 再把子序列按上述操作进行调整,直到所有的子序列中都只有一个元素,此时序列就是有序的**(逐步二分、二叉排序树的思想,使用递归来实现)**
- 优点:每一趟不仅能确定一个元素的最终位置,还能对整体的趋势做出调整,时间效率较高
- 时间复杂度: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
选择排序(不稳定)
- 简单选择排序(不稳定)
- 将序列分为两部分,第一次从无序部分中选择一个最小值,放到有序部分的第一个位置
- 第二次从无序部分中再选择一个最小值,放到有序部分的第二个位置
- 直到全部变为有序序列( n − 1 n-1 n−1次选择)
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 堆排序(不稳定)
- 首先将整个序列建立为大根堆(二叉树中的根节点最大)
- 然后将堆顶元素与最后一个元素进行交换,同时输出堆顶元素
- 重新建立大根堆,并循环执行元素交换与堆顶元素输出,直至排序完成
- 优点在于,堆排序记住了先前的排序结果,避免了重复的交换过程
- 时间复杂度: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
归并排序(稳定)
- 核心是分治思想,归并时用到了双指针操作
- 首先选取整个序列的中间位置,作为基准位置
- 接着分别对基准位置的左右序列进行递归
- 先递归,在归并
- 合并的时候是按照两个序列各个元素从小到大的顺序进行的归并
- 直到最后合并成一个有序序列
- 时间复杂度: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
基数排序(稳定)
-
前提:关键字为数字,才能按照位,进行分配和收集
-
主要操作步骤
- 分配:按照位,分配到0~9这十个序列内
- 收集:将分配完的数据,按顺序重新整合成一个序列
- 之后继续向高位进行分配和收集,直到分配到最大位
- 分配和收集的次数:最大关键字的位数 d d d
-
时间复杂度: O ( d + n ) O(d+n) O(d+n)
内部排序总结
- 对于数据规模较小的情况,直接插入排序、冒泡排序、简单选择排序( O ( n 2 ) O(n^2) O(n2))
- 对于数据规模中等的情况,希尔排序( O ( n 1.3 − n 1.5 ) O(n^{1.3}-n^{1.5}) O(n1.3−n1.5))
- 对于数据规模较大的情况,快速排序、堆排序、归并排序、基数排序,其中快排和堆排序都是不稳定的,而归并排序和基数排序是稳定的
面试真题
- 点操作和箭头操作的区别
- **相同点:**两个都是二元操作符,其右操作符是成员的名称
- 区别
- 简单来说,结构体用点,结构体指针用箭头
- 点操作".",左边的操作数是一个“结果为结构体”的表达式,用结构体名加“.”加成员名就可以引用成员了,就如同int a一样
- 箭头操作"->",则要声明一个结构体的指针,还要手动开辟一个该结构体的内存,然后把返回的指针给声明的结构体指针,才能使用"->"
- 此外,(*a).b 等价于 a->b
- 循环和递归的区别
- 循环:反复执行某一段代码,如果不加控制,就会形成死循环
- 优点是时空效率高,运行时间只因循环次数增加而增加,没什么额外开销,空间上也没有什么增加
- 缺点就是不容易理解,编写复杂问题时困难
- 递归:函数体中调用自己,如果不加控制,将无休止的调用自己,直到堆栈溢出
- 优点就是代码简单,易理解,容易编程
- 缺点是递归用栈机制实现的(本质上是后进先出),每深入一层,都要占去一块栈数据区域,而且递归也带来了大量的函数调用,这也有许多额外的时间开销,所以在深度大时,它的时空性就不好了
- 循环:反复执行某一段代码,如果不加控制,就会形成死循环
- C++中new和malloc的区别
- 最大的区别:new在申请空间时,会调用构造函数,而malloc则不会调用
- 在属性上,new是C++的一个关键字,需要编译器的支持,而malloc是库函数,需要添加头文件(stdlib.h)
- 使用的参数方面,new在申请内存分配时不需要指定内存块大小,编译器会根据申请的类型计算出大小,而malloc需要指定所需内存的大小
- 如何判断图里有环?
- 拓扑排序(有环图一定无拓扑排序)
- 主要思想就是根据每个结点的度,删除度小于等于1的结点及其相邻的边
- 然后看是不是还有度小于等于1的结点,如果有的话,就继续删除,要不然就有环了
- DFS(深度优先搜索)
- 如果在遍历的过程中,发现某个结点有一条边指向已访问过的结点
- 并且这个已访问过的结点不是上一步访问的结点,则表示存在环
- 拓扑排序(有环图一定无拓扑排序)
- Java和C++区别
- Java是纯面向对象的语言,C++既有面向对象又有面向过程的部分
- C++运行速度比Java快,Java具有比C++更好的跨平台性
- Java没有指针,C++有
- 内联函数定义及作用
- 如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline
- 在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符
- 引入内联函数的目的是为了提高函数调用的效率
- 一般的调用函数流程为:当前调用命令的地址被保存下来,程序流跳转到所调用的函数并执行该函数,最后跳转回之前所保存的命令地址,继续执行原来的程序
- 而调用内联函数时就直接把该函数的机器码插入到调用它的地方。提高了程序执行的效率
- 面向对象和面向过程的区别
- 面向对象:把整个需求按照特点、功能划分,将这些存在共性的部分封装成对象
- 优点:容易维护、方便代码复用和应用扩展
- 缺点:消耗资源,运行性能低
- 面向过程:分析出实现需求所需的步骤,通过函数一步一步实现这些步骤,接着依次调用
- 优点:运行性能好,且容易理解
- 缺点:不易维护、复用和扩展
- 面向对象:把整个需求按照特点、功能划分,将这些存在共性的部分封装成对象
- 重载和重写的区别
- 重载是在一个类中,定义相同的方法名,不同的参数列表,对返回类型和访问权限没有要求
- 体现的是编译时的多态性
- 重写是在子类与父类之间,定义相同的方法名,参数列表、返回类型必须相同,且重写的访问权限不能低于被重写的访问权限
- 体现的是运行时的多态性
- 重载是在一个类中,定义相同的方法名,不同的参数列表,对返回类型和访问权限没有要求
- 树和图的区别
- 树是分层结构,图是网络模型结构
- 树有根节点,图没有根节点的概念
- 树的边数为节点数减一,图的边数没有限制
- 树不能有环,图可以有
- 函数复用
- 其实就是将代码封装成一个函数,体现的是函数模块化设计
- 在C++中可以使用多态来实现
- C++命名规则
- 以字母 A-Z 或 a-z 或下划线 _ 开始,后跟零个或多个字母、下划线和数字(0-9)
- 递推的本质
- 寻找相邻两项之间的某种关系,通过一个公式或者一个规则来建立联系,从而实现递推(例如斐波那契数列1 1 2 3 5 8 13 … )
- 从n开始递推,从n推到n-1,一直推到比较小的数,就可以直接计算了
- 快速幂
- 一种快速计算整数幂的小算法
- 如果采用递归快速幂,本质是二分思想
- 计算 a n a^n an,如果 n n n是偶数(不为0),那么就先计算 a n / 2 a^{n/2} an/2,然后平方
- 如果 n n n是奇数,那么就先计算 a n − 1 a^{n-1} an−1,再乘上a;递归出口是** a 0 = 1 a^0=1 a0=1**
- 编译型和解释型语言区别
- 编译型语言:在程序执行之前,有一个单独的编译过程,将程序翻译成机器语言,以后执行这个程序的时候,就不用再进行翻译了
- 要求必须提前将所有源代码一次性转换成二进制指令,也就是生成一个可执行程序
- 使用的转换工具称为编译器
- C/C++ 、汇编语言
- 解释型语言:是在运行的时候将程序翻译成机器语言,所以运行速度相对于编译型语言要慢
- 一边执行一边转换,需要哪些源代码就转换哪些源代码,不会生成可执行程序
- 使用的转换工具称为解释器
- Python,MATLAB,Java,C#
- C语言的main函数参数列表有哪些?
- main (int argc, char *argv[])
- argc(第一个形参)必须是整型变量,参数的个数
- argv(第二个形参)必须是指向字符串的指针数组,参数的值,以字符串的格式存储
- 内存对齐
- 计算机中的内存空间都是按照byte划分的
- 系统对基本数据在内存中的存放位置会有限制,要求这些数据的首地址是某个数的倍数(通常为4或8),这就是所谓的内存对齐
- 举例:int占4byte,char占1byte,那么将它们放到一个结构体中应该占4+1=5byte;但是实际上,通过运行程序得到的结果是8 byte