什么是数据结构和算法(一)

1.1 数据结构和算法的概念

数据结构与算法相辅相成,不会孤立存在;数据结构是为算法服务的,算法是作用在特定的数据结构之上(如数组具有随机访问的特点,二分查找算法需要用数组来存储数据)。

数据结构是静态的,只是组织数据的一种形式,若不在它的基础上操作、构建算法、孤立存在的数据结构是没有意义的。

两种层面区分数据结构和算法

  • 广义:
    • 数据结构:一组数据的存储结构
    • 算法:操作数据的一组方法
  • 狭义:
    • 指的是某些著名的数据结构和算法,如:队列、栈、堆、二分查找、动态规划等

为什么学数据结构和算法

学好算法写出 时间复杂度高、空间复杂度高的垃圾代码也会越来越少,可以提升自己的代码性能,从而使自己的编程水平得到质的飞升。

建立时间、空间复杂度意识,写出高质量代码,能够设计基础架构,提升编程技能,训练逻辑思维,积攒人生经验,以此获得工作回报,实现你的价值,完善人生。

1.2 算法的五大特性

  • 输入:算法具有0个或多个输入
  • 输出:算法至少有1个或多个输出
  • 有穷性:算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成
  • 确定性:算法中的每一步都有确定的含义,不会出现二义性
  • 可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成

1.3 算法的提出

如果 a+b+c=1000,且 a2+b2=c^2(a,b,c 为自然数),如何求出所有a、b、c可能的组合?

1、方法一:

import time

start_time = time.time()
for a in range(1001):
    for b in range(1001):
        for c in range(1001):
            if a + b + c == 1000 and a**2 + b**2 == c**2:
                print(a, b, c)

end_time = time.time()
print('总耗时:', end_time-start_time)

运行结果如下:

0 500 500
200 375 425
375 200 425
500 0 500
总耗时:300.36014199256897

2、方法二:

import time

start_time = time.time()
for a in range(1001):
    for b in range(1001):
        c = 1000 - a - b
        if a**2 + b**2 == c**2:
            print(a, b, c)

end_time = time.time()
print('总耗时:', end_time-start_time)

运行结果如下:

0 500 500
200 375 425
375 200 425
500 0 500
总耗时:2.6143789291381836

同样的一道题,只是解题的思路不同,性能却是天差地别,这就是算法的魅力。

1.4 大 O 表示法

理论上来讲依靠执行时间就能衡量一个算法的效率,但若将两个相同的算法放在不同配置的机器上运行,那么单纯地依靠运行时间来衡量,显然是不合理的。那么应该用什么方式来衡量一个算法的效率呢?

算法效率衡量

假设计算机执行算法每一个基本操作的时间是固定的一个时间单位,那么多少个操作就有多少个时间单位。可能每台机器的单位时间不同,但是对算法来说进行多少个基本操作在规模数量级上是相同的,由此可忽略机器环境(配置/网络)对算法的客观影响。


大 O 表示法

下面我们通过两个小示例来理解什么是 大 O 表示法

假设每行代码执行时间为:unit_time

示例一:

def func():
    a = 3               # 1
    b = 9               # 2
    for i in range(n):      # 3
        c = a*i + b         # 4
        return c

第1 、2 行代码各花费 一个 unit_time,而第 3 、4 行需要执行 n 次,各花费 n * unit_time,那么代码总执行时间 T(n) 即为:T(n) = (2n + 2)*unit_time


示例二:

def func():
    a = 3               # 1
    b = 9               # 2
    for i in range(n):      # 3
        for j in range(n):  # 4
            c = a*i + b + j     # 5
            return c
  • 第1 、2 行:2 * unit_time
  • 第 3 行:n * unit_time
  • 第 4 、5 行:每一行都是 n^2 * unit_time,那么总共为 2n^2 * unit_time,因为有内外两层循环

那么代码总执行时间为:T(n) = (2n^2 + n + 2) * unit_time

根据上面两个例子的推导,可以得出一个规律:“所有代码执行时间 T(n)与每行代码执行次数 n 成正比”

将其总结成一个公式,即为 大 O 时间复杂度分析法

T(n) = O(f(n))
  • T(n):代码总执行时间
  • n:表示数据规模大小
  • f(n):表示每行代码执行的次数总和
  • O:表示代码的执行时间 T(n) 与 f(n) 成正比

当 n 很大时,公式中的 低阶、常量、系数三部分几乎不左右增长趋势,常常忽略不计,而只取 最大量级。所以上述两个例子用 大 O 复杂度表示法表示为:T(n) = O(n)T(n) = O(n^2)

注意:大 O 时间复杂度不具体表示代码真正运行时间,而是表示代码执行时间随数据规模增长的变化趋势,也叫做 ——渐进时间复杂度 (asymptotic time complexity),简称时间复杂度

1.5 时间复杂度分析

如何分析一段代码的时间复杂度,有三个方法:

  • 只关注循环次数最多的一段代码:因为低阶、系数、常量随着规模的增长对变化趋势没有很多的影响,所以只取最大量级的,即循环次数最多的那部分。
  • 加法法则:总复杂度等于量级最大的那段代码的复杂度(如:T(n) = (2n^2 + n + 2) * unit_time,只取 n^2
  • 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
# 若 T1(n) = O(f(n)),    T2(n) = O(g(n)),那么
T(n) = T1(n) * T2(n) = O(f(n)) * O(g(n)) = O(f(n) * g(n))

# 假设 T1(n) = O(n), T2(n) = O(n^2),那么
T(n) = T1(n) * T2(n) = O(n * n^2) = O(n^3)

1、只关注循环次数最多的代码:

def func(n):
    a = 0
    b = 1
    c = 0
    for i in range(n):  # 循环 n 次
        c += i
    return c

第2/3/4 行都是常量级的执行时间,与 n 大小无关,因此对整个复杂度也没有影响。第 6 行循环执行了 n 次,因此总的时间复杂度为 O(n),只需要关心循环次数最多的代码。

2、加法法则:总复杂度等于量级最大的那段代码的复杂度。

def func(n):
    sum_1, p = 0, 1
    while p < 100:
        sum_1 += sum_1 + p
        p += 1
        
    sum_2, q = 0, 1
    for i in range(n):
        sum_2 += i
        
    sum_3, i, j = 0, 1, 1
    for i in range(n):
        j = 1
        for j in range(n):
            sum_3 += i * j
            
     return sum_1, sum_2, sum_3

整段代码可以分为三部分,分别是求:sum_1、sum_2、sum_3 的值,每一部分的时间复杂度为:

  • sum_1:执行了 100 次,但是是常量级,可忽略,为 O(1)
  • sum_2:循环执行 n 次,为 O(n)
  • sum_3:两次循环执行 n 次,为 O(n^2)

总的时间复杂度为:O(1) + O(n) + O(n^2),取最大量级为:O(n^2)

3、乘法法则:

嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

def func(m, n):
    a, b, sum = 0, 0, 0
    for i in range(n):
        a = b + i
        for p in m:
            sum = a + p
            
     return sum

时间复杂度为:O(n) * O(n) = O(n^2)


最坏时间复杂度

分析算法时,存在几种可能的考虑:

  • 算法完成工作最少需要多少基本操作,即最优时间复杂度
  • 算法完成工作最多需要多少基本操作,即最坏时间复杂度
  • 算法完成工作平均需要多少基本操作,即平均时间复杂度

最好时间复杂度并没有什么实际意义,往往需要特殊情况才能达到。平均时间复杂度是对这个算法的全面评价,反映了这个算法的本质,但是这种衡量没有保证,不是每个计算都能保证在这个操作内完成。因此我们更应该关心的是最坏时间复杂度,因为不管怎样在这个操作内,都能完成。


1.6 算法分析

现在我们已经知道什么是时间复杂度,也知道怎么用大 O 表示法,现在我们来分析下开头的两个示例各自的时间复杂度。

for a in range(1001):
    for b in range(1001):
        for c in range(1001):
            if a + b + c == 1000 and a**2 + b**2 == c**2:
                print(a, b, c)

我们只关注循环次数最多的,根据乘法法则,得出应为:T(n) = (n*n*n)*unit_timeO(n^3)

for a in range(1001):
    for b in range(1001):
        c = 1000 - a - b
        if a**2 + b**2 == c**2:
            print(a, b, c)

根据加法法则和乘法法则,可得出为:T(n) = (n*n + n*n) * unit_timeO(n^2)

随着规模的增长,显然第二个方法要比第一个方法快得多。

1.7 常见时间复杂度

执行次数函数举例 非正式术语
12 O(1) 常数阶
2n+3 O(n) 线性阶
3n^2+2n+1 O(n^2) 平方阶
5log2n+20 O(logn) 对数阶
2n+3nlog2n+19 O(nlogn) 线性对数阶
6n3+2n2+3n+4 O(n^3) 立方阶
2^n O(2^n) 指数阶
n! O(n!) 阶乘阶
n^k O(n^k) k 次方阶

Tips:log2n(以2为底的对数)简写成 logn

常见时间复杂度对比

O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2^n) < O(n!) < O(nn)

常见时间复杂度对比

1.8 Python 内置类型分析

Python 给我们提供了一个 timit 模块,可以用来测试一小段 Python 代码的执行速度。

# 类 Timer 是测量小段代码执行速度的类
Timer(stmt='pass', setup='pass', timer=<timer function>)

# stmt参数是要测试的代码语句(statment)
# setup参数是运行代码时需要的设置
# timer参数是一个定时器函数,与平台有关

# timeit() 方法,执行速度的对象方法,number参数是测试代码时的测试次数,默认为1000000次,返回执行代码的平均耗时,一个float类型的秒数
timeit(number=1000000)

示例

from timeit import Timer

def test1():
    """列表相加"""
    l = []
    for i in range(1000):
        l = l + [i]

def test2():
    """append 尾部添加"""
    l = []
    for i in range(1000):
        l.append(i)

def test3():
    """列表生成式"""
    l = [i for i in range(1000)]


def test4():
    """range() 方法"""
    l = list(range(1000))

t1 = Timer('test1()', 'from __main__ import test1')
t2 = Timer('test2()', 'from __main__ import test2')
t3 = Timer('test3()', 'from __main__ import test3')
t4 = Timer('test4()', 'from __main__ import test4')

print('test1 总耗时:', t1.timeit(number=1000),  '秒')
print('test2 总耗时:', t2.timeit(number=1000),  '秒')
print('test3 总耗时:', t3.timeit(number=1000),  '秒')
print('test4 总耗时:', t4.timeit(number=1000),  '秒')

运行结果:

test1 总耗时: 2.0456191 秒
test2 总耗时: 0.10657389999999989 秒
test3 总耗时: 0.03904730000000001 秒
test4 总耗时: 0.016495899999999786 秒

list pop 测试

from timeit import Timer

x = list(range(2000000))
pop_zero = Timer("x.pop(0)","from __main__ import x")
print("pop_zero ",pop_zero.timeit(number=1000), "seconds")

y = list(range(2000000))
pop_end = Timer("y.pop()","from __main__ import y")
print("pop_end ",pop_end.timeit(number=1000), "seconds")

运行结果:

pop_zero  2.4630802 seconds
pop_end  0.00010299999999974219 seconds

列表常见操作的时间复杂度

操作 时间复杂度 操作 时间复杂度
index() O(1) index assignment 索引赋值 O(1)
append() O(1) pop() O(1)
pop(i) O(n) insert(i, item) O(n)
del list O(n) iteration 迭代 O(n)
contains(in) 包含 O(n) get slice [x:y] 分片 O(k)
set slice O(n + k) reverse() 翻转 O(n)
concatenate 两个列表拼接 O(k) sort() 排序 O(nlogn)
multiply 两个列表相乘 O(nk)

字典常见操作时间复杂度

操作 时间复杂度 操作 时间复杂度
copy O(n) get(item) O(1)
set(item) O(1) delete item O(1)
contains(in) O(1) iteration 迭代 O(n)

1.9 常用数据结构的算法

要想搞懂什么是数据结构,就要懂得什么是数据,什么又是结构?

  • 数据是一种抽象的概念,我们经常与之打交道,它有不同的存储类型,就Python 中有:int、float、list、dict 等。

  • 数据之间存储不是相互独立的,而是以一种特殊的关系联系在一起,这就是结构。

Python 给我们提供了很多内置的数据结构类型:int、float、list、dict...,而有一些并没有提供,需要我们自己去定义,如:队列、栈等。


算法与数据结构区别

  • 数据结构是存储数据的一种表现形式
  • 算法是解决问题的一种设计

一个好的程序应该是在合适的数据结构基础上选择算法。一个合适的数据结构非常重要,比如:存储全世界人们的个人信息,如果用列表存储。那么遍历时其时间复杂度为 O(n),而用字典存储,遍历其时间复杂度为 O(1)


常用的数据运算

插入、删除、查找、修改、排序


常用的二十种数据结构和算法

在实际开发中,我们能够经常用到的有如下 20种,数据结构和算法各 10种:

  • 数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树
  • 算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,367评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,959评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,750评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,226评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,252评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,975评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,592评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,497评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,027评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,147评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,274评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,953评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,623评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,143评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,260评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,607评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,271评论 2 358