程序员基本功系列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) 就会被计算多次。 

posted @ 2022-01-28 14:26  jingyi_up  阅读(69)  评论(0编辑  收藏  举报