程序员基本功系列1——算法与数据结构基础
1、衡量算法的标准
衡量算法的两个维度:时间复杂度和空间复杂度。
(1)时间复杂度
时间复杂度一般用大 O 时间复杂度表示,它并不是具体指代码真正执行的时间,而是代码执行时间随着数据规模增长的变化趋势。
三个判断方法:
• 只关注循环执行次数最多的一段代码
• 加法法则
• 乘法法则:嵌套代码的时间复杂度等于嵌套内外代码复杂度的乘积
几种常见时间复杂度:
常量阶—O(1),对数阶—O(logn),线性阶—O(n),线性对数阶—O(nlogn),平方阶或k次方阶—O(nk)
时间复杂度根据情况还分为:最好时间复杂度、最坏时间复杂度、平均时间复杂度、均摊时间复杂度。
(2)空间复杂度
常见空间复杂度:O(1)、O(n)、O(n2)
2、数据结构
数据结构可以分为:
线性表:最多只有前后两个方向,包括数组、链表、队列、栈。
非线性表:二叉树、堆、图。
2.1、数组
数组用一组连续的内存空间来存储一组具有相同类型的数据。
数组元素访问寻址算法:
a[i]_address = base_address + i * data_type_size
其中:base_address 是内存块首地址,如数组存储的 int 类型,则 data_type_size 为四个字节。
数据优点:可以根据下标随机访问,缺点:低效的删除和插入,为了保持连续性,需要做大量的数据迁移。
2.2、链表
分为:单向链表、循环链表、双向链表。
查询的时间复杂度是 O(n),插入和删除的时间复杂度是 O(1)。
应用案例:双向链表实现 LRU 淘汰算法
要点:访问时,如果数据不在链表中,直接在头部插入,否则,要先找到数据所在位置,再将数据节点移到头部,要做到时间复杂度 O(1),可以使用哈希表来存储数据对应的节点指针。
链表与数组的比较:
• 数组需要连续的内存,对于内存要求较高,链表则不需要连续内存,自带动态扩容,但是存在额外的指针(前驱和后驱指针)会增加内存开销,并且频繁的插入和删除更容易产生内存碎片和触发垃圾回收。
• 数组对于处理器的缓存机制更加友好,更加有利于发挥操作系统缓存行的特性,提升性能。美团《高性能队列》一文中有提及这一点。
缓存行:CPU缓存有多个缓存行组成(Cache line,一般64个字节),CPU从主内存中拉取数据时,会把相邻数据一起拉下来放入到缓存行中。
2.3、栈
特性:先进后出
可以由数组实现(顺序栈)或链表实现(链表栈)
应用案例:
(1)浏览器页面的前进后退
使用两个栈,X 和 Y,把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。
(2)表达式运算
例如 3+5*8-6 这个表达式。实际上编译器就是通过两个栈实现的,其中一个保存操作数的栈,另一个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。
(3)有效括号的判断
假设表达式中只包含三种括号,圆括号 ()、方括号[]和花括号{},并且它们可以任意嵌套。比如,{[] ()[{}]}或[{()}([])]等都为合法格式,而{[}()]或[({)]为不合法的格式。给一个包含三种括号的表达式字符串,如何检查它是否合法呢?
也可以用栈来解决。用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。
2.4、队列
特性:先进先出
也可以由数组或链表实现,此外还有阻塞队列,并发队列,在队列数据结构的基础上增加了入队和出队的阻塞操作,以及多线程下的同步机制。
3、算法
3.1、递归思想
递归不是具体的算法,但在很多算法都会用到,比如 DFS深度优先搜索、前中后序二叉树遍历等,所以理解好递归是很必要的。
递归需要满足的三个条件:
• 一个问题的解可以分解为几个子问题的解
• 这个问题与分解的子问题,除了数据规模求解的思路一样
• 存在终止条件
写递归代码的关键:
• 写出递归公式 • 找到终止条件
理解递归要注意的点:
• 只要将问题抽象成递推公式,千万不要去想一层层的调用关系
• 要避免重复的计算,如:f(5) = f(4)+f(3),f(4) = f(3)+f(2),其中 f(3) 就会被计算多次。