测试开发算法系列-数据结构与算法入门

740 阅读6分钟

1、为什么学习算法

  1. 学习算法的目标首先是面试,现在大厂的面试几乎是首先要求算法题在中等级别以上
  2. 有利于写出更加高效的代码,简明的代码
  3. 更容易理解计算机工作过程,包括各种数据结构的理解

2、复杂度计算

复杂度计算分为2种,时间复杂度和空间复杂度

最简单粗暴的理解就是时间复杂度指的是运行程序需要多长时间,空间复杂度指的是运行程序需要多少内存

def fun(n):
    return n%2 == 0
# 该程序运行需要一次计算和一个变量n,所以时间复杂度为O(1),空间复杂度为O(1)
def fun(n):
    res = []
    for i in range(n):
        res.append(i)
    return res
# 该程序运行需要n次计算,需要1个n长的列表,所以时间复杂度为O(n),空间复杂度为O(n)
def fun(n):
    res = []
    for i in range(n):
        if i%2 == 0:
            res.append(i)
    return res
# 该程序运行需要n次判断,n次append操作,append本身是1O(1)的操作,所以时间复杂度为O(2n),空间复杂度为O(n/2),通常会忽略常量,所以结果就是时间复杂度O(n),空间复杂度O(n)

3、数据结构-数组,链表,队列

1. 数组:数组是连续的内存空间存储的数据结构,在python3中List就是数组结构

image.png 从上面的图开始分析数组各种操作(增删查改)的复杂度

append(尾部添加): 大部分情况下都是O(1),只有当内存分配的内存空间不足才会需要重新复制迁移数据

demo_list = [1, 2, 3, 4]
demo_list.append(5)
print(demo_list)

image.png

pop(尾部弹出):复杂度也是O(1),这个就不画图介绍了

get(根据索引位置获取元素),复杂度O(1),从下图看get元素只有1次计算,0x0001+2=0x0003

image.png

set(设置元素的值):和get一样,计算出元素在哪个内存位置替换掉,2步操作,复杂度O(1)

insert(中间插入元素):需要移动插入之后的所有元素,时间复杂度是O(n)

image.png

image.png

2. 链表:内存不连续的数据链式数据结构

  • 2.1. 链表在底层数据的存储结构(存储1个a-b-c的链表) image.png
  • 2.2. 实际python3是如何定义链表

image.png

  • 2.3. python3创建1个1->2->3的链表,代码该如何写 先用图例展示,然后用最简单的方式实现创建1个链表

image.png

# 首先要定义1个Node对象,这个对象就是data,next这2个字段组成的1个对象

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node1.next = node2
node2.next = node3
node3.next = None
head = node1
print(head)

从debug上可以看到链表存到了head的变量中,第1个节点的值是1,下一个节点的值是2,下下个节点的值是3 image.png

  • 2.4. 实际编程中肯定不会通过这种方式去创建1个链表,如果有1万个值,那么需要手动写一万次node.next=node1,实际创建链表的代码如下
class Node:

    def __init__(self, value):
        self.value = value
        self.next = None


class LinkList:

    @staticmethod
    def create_link_list(*args):
        if not args:
            return None
        if len(args) == 1:
            return Node(args[1])
        head = Node(args[0])
        pre = head
        for i in range(1, len(args)):
            node = Node(args[i])
            pre.next = node
            pre = node
        return head

image.png 各种操作(增删查改)的复杂度计算

插入一个元素:复杂度是O(1),查看下面的图,只需要3步,常量级别

image.png

删除一个元素:时间复杂度为O(1),查看下面的图,只需要3步,常量级别

image.png

查找一个元素:时间复杂度为O(n),需要从第1个节点开始查找

link = LinkList.create_link_list(1, 2, 3, 4, 5)
node = link
for i in range(4):
    node = node.next
print(node.value)

修改一个元素的值:时间复杂度为O(n),也是需要从第1个节点开始查找

4、算法-青蛙跳台阶

青蛙跳台阶,每次可以跳1个台阶或者2个台阶,总共有n个台阶,青蛙有多少种跳法 举例:

1个台阶,青蛙的跳法:1

2个台阶:青蛙的跳法:2,2个1步+1个2步

3个台阶:青蛙的跳法 [1,1,1] [1,2] [2,1]

image.png

解题思路:

1、采用分而治之的思路,青蛙在最后一次跳跃的时候有2种可能,在倒数第2阶跳1步,或者倒数第3阶跳2阶

2、假设总共是4个台阶,青蛙无论在前面怎么跳,到最后都是要么从第3阶跳1步到第4阶,要么跳2步到第4阶

3、假设跳到n阶有f(n)种跳法,那么跳到n-1就是f(n-1)种,跳到n-2阶就是f(n-2)

4、从以上条件可以得出结论,青蛙一定需要先跳到n-1个台阶在跳1步到n个台阶,或者先跳到n-2个台阶再跳2步到n个台阶,所以跳到n个台阶f(n) = f(n-1) + f(n-2)

结论:n=1时,f(1)=1 n=2, f(2)=2 f(n) = f(n-1) + f(n-2)

def f(n):
    if n == 1:
        return 1
    if n == 2:
        return 2
    return f(n-1) + f(n-2)

如果在面试时,面试官肯定会问,你这个代码有什么问题

问题:

1、效率问题:这个代码的时间复杂度是多少?

2、递归调用一定会出现栈溢出, stackOverFlowError

时间复杂度计算:

image.png 第1问题被分解成2个,2个分解成4个,4个会被分解成8个,直到最后一层变成1或者2,也就是时间复杂度为O(2^n),这个运算速度会非常慢

import time
print(time.ctime())
print(f(37))
print(time.ctime())

image.png 计算跳37个台阶,总共花了7秒,这个在程序中是不能接受的

栈溢出问题:

# 传入1000个台阶,程序会抛出异常
import time
print(time.ctime())
print(f(1000))
print(time.ctime())

image.png python3中最大递归深度,最大是999

解决方案: 使用非递归代码,通过a,b 2个变量,不停的循环,我们查看效果

def fib1(n):
    if n == 1:
        return 1
    if n == 2:
        return 2
    temp = 2
    a, b = 1, 2
    while temp < n:
        a, b = b, a + b
        temp += 1
    return b
    
import time
print(time.ctime())
print(fib1(1001))
print(time.ctime())

image.png 计算结果很快,且不存在栈溢出问题

5、关于后续

本次只是简单介绍了时间复杂度,数据和链表,递归计算和优化,数据结构还包含其他很多的内容

  1. 队列
  2. 树形数据结构,二叉树,B+数,红黑树
  3. 股票买卖,动态规划,排序,哈希等

列表和元组的区别:

1、列表可变,元组不可变

2、列表不可哈希,不可作为字典的key,元组可哈希,可作为字典的key

3、列表中in是复杂度是O(n),元组是O(1),元组是哈希表结构