数据结构与算法

1.算法性能评估-python
概论
时间复杂度
空间复杂度
时间复杂度
规模: 不同量级有不同的速度,比如水 vs 水杯: 水
测试环境: 在不同测试环境,速度也不同,比如手机 vs 电脑: 电脑
大 O 表示法
def tmp(n):
add = 0
for i in range(n):
add += i
return add
运行时间:T(n) = (2n + 1) * unit
T(n) = O(f(n)) , O表示 T(n) 与 f(n) 成正比
O 表示渐近时间复杂度
表示代码执行时间随数据规模增长的变化趋势
当 n 很大时,低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略.就可以记为:T(n) = O(n); T(n) = O(n²)。
只关注循环次数多的代码
def tmp(n):
add = 0
for i in range(n):
add += i
return add

O(n)
选大量级
def tmp(n):
for i in range(999):
print(123)

for i in range(n):
    print(1)

for i in range(n):
    for j in range(n):
        print(2)

O(n²)
嵌套循环要乘积
def tmp(n):
for i in range(n):
a(i)

def a(n):
for i in range(n):
print(‘c’)

O(n²)
常见复杂度分析
非多项式量级(过于低效) : O(2ⁿ) 和 O(n!)。
多项式量级:O(1), O(logn), O(n), O(nlogn), O(nk)
O(1)
a=2
b=3
d=4
O(logn)
def tmp(n):
i = 1
while i < n :
i = i * 2

i = 2º,2¹, 2², 2³…2^x
退出循环的条件是 : 2^x = n ,即 x = log2n,时间复杂度为 O(log2n)
def tmp(n):
i = 1
while i < n :
i = i * 3

log3n 就等于 log32 * log2n,所以 O(log3n) = O(C * log2n),其中 C=log32 是一个常量。基于我们前面的一个理论:在采用大 O 标记复杂度的时候,可以忽略系数,即 O(Cf(n)) = O(f(n))。所以,O(log2n) 就等于 O(log3n)。因此,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O(logn)
O(m+n)、O(m*n)
def tmp(m, n):
for i in range(m):
print(1)

for i in range(n):
    print(2)

空间复杂度
渐进时间复杂度:表示算法的执行时间与数据规模之间的增长关系。
渐进空间复杂度:表示算法的存储空间与数据规模之间的增长关系。

def tmp(n):
a = [1]*n
for i in a:
print(i)

空间复杂度是: O(n)

2.数组与列表-python

什么是数组?
数组是线性表数据结构。用连续的内存空间存储相同类型的数据。
线性表:线性表是数据排成一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。
包括:数组,链表、队列、栈。
非线性表:数据之间并不是简单的前后关系。
包括:二叉树、堆、图等。
连续的内存空间和相同类型的数据:使数组支持“随机访问”。但在数组中删除、插入数据时,需要做大量的数据搬移工作。
数组如何实现随机访问?
C 语言代码: int[] tmp = new int[4],这个数组在内存中连续放置:

image
插入操作
在数组末尾插入元素,不需要移动数据,时间复杂度为 O(1)。
在数组开头插入元素,那所有的数据都需要依次往后移动一位,所以最坏时间复杂度是 O(n)。
假设每个位置插入元素概率相同,平均情况时间复杂度为 (1+2+…n)/n=O(n)。

image846×290 1.17 KB
image

如果元素无序,可直接换位:
删除操作
删除数组末尾数据:则最好情况时间复杂度为 O(1);
删除开头的数据:则最坏情况时间复杂度为 O(n);
平均情况时间复杂度也为 O(n)。

image846×290 1.17 KB
image

预删除思想:JVM 标记清除垃圾回收算法
改进数组
编程语言封装了数组,比如 Java 的 ArrayList, Python 的 List ,可实现自动扩容,多种数据类型组合。
如果你是底层工程师,需要极致的比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。
class Array:
def init(self, capacity) -> None:
self.data = [-1]*capacity
self.count = 0
self.n = capacity

def insert(self, location, value):
    if self.n == self.count:
        return False

    if location < 0 or location > self.count:
        return False

    for i in range(self.count, location, -1):
        self.data[i] = self.data[i-1]
    
    self.data[location] = value
    self.count += 1
    return True

def find(self, location):
    if location < 0 or location >= self.count:
        return -1
    
    return self.data[location]



def delete(self, location):
    if location < 0 or location >= self.count:
        return False
    
    for i in range(location + 1, self.count):
        self.data[i-1] = self.data[i]
    
    self.count -= 1
    return True

def test_demo():
array = Array(5)
array.insert(0, 1)
array.insert(0, 2)
array.insert(1, 3)
array.insert(2, 4)
array.insert(4, 5)

# 判断插入不成功
assert not array.insert(0, 100)
assert array.find(0) == 2
assert array.find(2) == 4
assert array.find(4) == 5
assert array.find(10) == -1
assert array.count == 5
removed = array.delete(4)
assert removed
assert array.find(4) == -1
removed = array.delete(10)
assert not removed
# 2 3 4 1 5
assert array.data == [2, 3, 4, 1, 5]

if name == ‘main’:
test_demo()
python list 源码解析
本篇文章描述了 CPython 中 list 的实现方式。
C 语言用结构体表示 List 对象
C 语言使用结构体实现 list 对象,结构体代码如下。
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item; //指向 list 中的对象
Py_ssize_t allocated; //内存分配的插槽
} PyListObject;

List 初始化
以 I = [] 为例

image1090×416 38.5 KB
image

list 的数量是指 len(l)。分配的槽位数量是指在内存中实际分配的数量。通常情况,内存中分配的数量要大于 list 的数量。这是为了当添加新元素时,避免内存再分配。
Append
当运行l.append(1)时, CPython 将调用app1():

image1054×331 23.8 KB
image

list_resize() 会故意分配更多的内存,避免被多次调用。分配内存大小增加:0, 4, 8, 16, 25, 35, 46, 58, 72, 88, …

image1068×357 33.1 KB
image

第一次分配了 4 个槽位,I[0] 指向了数字对象 1 。正方形虚线表示未使用过的槽位。追加操作的均摊复杂度为 O(1) 。
均摊时间复杂度是平均时间复杂度的一种,是一种简化的计算方法。

image
继续追加元素:l.append(2)。调用 list_resize 实现 n + 1 = 2。由于分配了四个空间,不需要分配内存。当再向列表追加两个数字时,l.append(3), l.append(4),如下图如示:

Python lists
Insert
在位置 1 插入整型 5 ,即调用 python 的 l.insert(1, 5) 。CPython 会调用 ins1() :

image1105×354 34 KB
image

插入操作需要将剩余元素向右迁移:

image
上图虚线表示未使用的槽位(slots),分配了 8 个槽位,但 list 的长度只有 5 。 insert 的时间复杂度为 O(n)。
pop
弹出列表的最后一个元素使用 l.pop(),CPython 使用 listpop() 实现这个过程。如果新内存大小少于分配大小的一半, listpop() 将调用 list_resize 减少 list 内存。

image1079×363 28.9 KB
image

Pop 的时间复杂度是 O(1)。

image
注意,此时槽位 4 仍然指向整型 4 ,但是 list 的大小却是 4 。只有 pop 更多的元素才能调用 list_resize() 减少内存,如果再 pop 一个元素, size - 1 = 4 - 3 = 3, 3 小于分配槽位的一半 8/2 = 4 。所以 list 收缩到 6 个槽位, list 的大小为 3 。虽然槽位 3 和 4 依旧指向整型对象,但是整体大小变成了 3 。

image

remove
Python 可以用 remove 删除指定元素:l.remove(5)。此时将调用 listremove() 。

image1084×366 29.4 KB
image

CPython 调用 list_ass_slice() 函数对列表进行切分并删除元素。当在位置 1 移除元素 5 时,低偏移(low offset)是 1 ,高偏移(high offset)是 2 :

image1101×285 25.6 KB
image

remove 时间复杂度是 O(n)。

image1045×752 61.9 KB
image

文章参考:
http://www.laurentluce.com/posts/python-list-implementation/

堆栈-python
引言

函数调用
编译原理:语法分析
括号匹配
问题:栈是操作受限的线性表,为什么不直接用数组或者链表?
数组和链表暴露了太多接口,操作虽然灵活,但不可控,容易出错。比如数组支持任意位置插入数组,如果插入位置写错将改变所有数组的内容。
而栈只能在一端插入和删除数据,并且后进先出。
顺序栈
使用数组实现栈

class ArrayStack:
def init(self, n) -> None:
self.data = [-1]*n
self.n = n
self.count = 0

def push(self, value):
    if self.n == self.count:
        return False
    self.data[self.count] = value
    self.count += 1
    return True

def pop(self):
    if self.count == 0:
        return None

    self.count -= 1
    return self.data[self.count]

def test_static():
array_stack = ArrayStack(5)
data = [“a”, “b”, “c”, “d”, “e”]
for i in data:
array_stack.push(i)

result = array_stack.push("a")
assert not result
data.reverse()
for i in data:
    assert i == array_stack.pop()

assert array_stack.pop() is None

if name == ‘main’:
test_static()

入栈时间复杂度:O(1)出栈时间复杂度:O(1)
链式栈
使用链表实现栈

class StackBasedOnLinkedList:
def init(self) -> None:
self.top = None

def push(self, value):
    new_node = self.Node(value)
    if self.top is None:
        self.top = new_node
    else:
        new_node.next = self.top
        self.top = new_node

def pop(self):
    if self.top is None:
        return -1
    result = self.top.data
    self.top = self.top.next
    return result

class Node:
    def __init__(self, data) -> None:
        self.data = data
        self.next = None

def test_static():
stack = StackBasedOnLinkedList()
data = [1, 2, 3, 4, 5]
for i in data:
stack.push(i)
data.reverse()
for i in data:
assert i == stack.pop()
assert stack.pop() == -1

入栈时间复杂度:O(1)出栈时间复杂度:O(1)

3.链表-python
链表与数组的区别

https://www.processon.com/diagraming/606c14aa7d9c0829db70c8a0

https://www.processon.com/diagraming/606c345cf346fb0aa989879c1125×1258 43.1 KB
https://www.processon.com/diagraming/606c345cf346fb0aa989879c

单链表与循环链表
注意链表中的头结点和尾结点。
循环链表从尾可以方便的到头,适合环型结构数据,比如约瑟夫问题。
双向链表
优势:
O(1) 时间复杂度找到前驱结点
删除,插入更高效。考虑以下两种情况:
删除结点中“值等于某个给定值”的结点 
删除给定指针指向的结点 
查询更高效:记录上次查找的位置 p,每次查询时,根据要查找的值与 p 的大小关系,决定是往前还是往后查找,所以平均只需要查找一半的数据。
再次比较数组和链表
从复杂度分析:
时间复杂度数组链表插入删除O(n)O(1)随机访问O(1)O(n)
其它角度:
内存连续,利用 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。
而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。
数组的大小固定,即使动态申请,也需要拷贝数据,费时费力。
链表支持动态扩容.
class SinglyLinkedList:
def init(self) -> None:
self.head = None

def insert_tail(self, value):
    if self.head is None:
        self.insert_to_head(value)
        return
    q = self.head
    # 寻找尾结点
    while q.next is not None:
        q = q.next
    new_node = self.Node(value)
    q.next = new_node

def insert_to_head(self, value):
    new_node = self.Node(value)
    if self.head is None:
        self.head = new_node
    else:
        new_node.next = self.head
        self.head = new_node

def delete_by_value(self, value):
    if self.head is None:
        return False
    q = self.head
    p = None
    while q is not None and q.data != value:
        p = q
        q = q.next
    # 当链表中没有 value 的时候
    if q is None:
        return False
    # head 的值就是 value 的时候
    if p is None:
        self.head = self.head.next
    else:
        p.next = q.next
    return True

def find_by_value(self, value):
    if self.head is None:
        return
    q = self.head
    while q is not None and q.data != value:
        q = q.next

    if q is None:
        return
    return q

def insert_after(self, node, value):
    if node is None:
        return
    new_node = self.Node(value)
    new_node.next = node.next
    node.next = new_node

def insert_before(self, node, value):
    if self.head is None:
        self.insert_to_head(value)
        return
    q = self.head
    while q is not None and q.next != node:
        q = q.next
    # 链表中,没有一个与 node 相等的结点
    if q is None:
        return
    new_node = self.Node(value)
    new_node.next = node
    q.next = new_node
    



def print_all(self):
    if self.head is None:
        return
    q = self.head
    while q is not None:
        print(q.data)
        q = q.next

class Node:
    def __init__(self, data) -> None:
        self.data = data
        self.next = None

def test_link():
link = SinglyLinkedList()
data = [1, 2, 5, 3, 1]
for i in data:
link.insert_tail(i)
link.insert_to_head(99)
# 打印内容为 99 1 2 5 3 1
link.print_all()
link.delete_by_value(2)
assert not link.delete_by_value(999)
assert link.delete_by_value(99)
# 打印内容为 1 5 3 1
link.print_all()
assert link.find_by_value(2) is None
new_node = link.find_by_value(3)
link.insert_after(new_node, 10)
assert link.find_by_value(3).next.data == 10
link.insert_before(new_node, 30)
assert link.find_by_value(5).next.data == 30

if name == ‘main’:
test_link()

二叉树-python
树的基本术语
父节点,子节点: A 和B兄弟节点:D E J根节点:A叶节点:G I

https://www.processon.com/mindmap/6073b23b7d9c081712e0b885
节点高度:节点到叶节点的最长路径(边数)
节点深度:根节点到这节点所经历的边的个数
节点的层数:节点的深度 + 1
树高度:根节点的高度

https://www.processon.com/mindmap/6073b23b7d9c081712e0b885
节点的高度:一个人量身体各部位长度,从脚下拉尺到各部位(腿长)
节点深度:量水中物品的深度,从水面拉尺到水中的宝藏
节点的层数:尺子从 1 开始计数
树高度:头到脚的高度
二叉树
最多有两个叉的树。
满二叉树:叶子节点全在最底层,除了叶子节点之外,每个节点都有左右两个子节点。

二叉树结构 (1)
完全二叉树:叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大。

二叉树结构 (2)
满二叉树和完全二叉树的意义是什么?

https://www.processon.com/diagraming/6073e482e401fd01e20e3b44939×2021 56.4 KB
https://www.processon.com/diagraming/6073e482e401fd01e20e3b44

节点 X 在数组下标 i
左子节点:2 * i
右子节点:2 * i + 1
父节点:i/2
二叉树的遍历

二叉树结构 (1)
根据节点打印的顺序分前,中,后。比如:
前序遍历:节点 → 左子树 → 右子树。A->B->D->E->C->F->G
中序遍历:左子树 → 节点 → 右子树。D->B->E->A->F->C->G
后序遍历:左子树 → 右子树 → 节点。D->E->B->F->G->C->A
递推关系式:

前序遍历的递推公式:
preOrder® = print r->preOrder(r->left)->preOrder(r->right)

中序遍历的递推公式:
inOrder® = inOrder(r->left)->print r->inOrder(r->right)

后序遍历的递推公式:
postOrder® = postOrder(r->left)->postOrder(r->right)->print r

代码:

void preOrder(Node* root) {
if (root == null) return;
print root // 此处为伪代码,表示打印root节点
preOrder(root->left);
preOrder(root->right);
}

void inOrder(Node* root) {
if (root == null) return;
inOrder(root->left);
print root // 此处为伪代码,表示打印root节点
inOrder(root->right);
}

void postOrder(Node* root) {
if (root == null) return;
postOrder(root->left);
postOrder(root->right);
print root // 此处为伪代码,表示打印root节点
}

复杂度:
O(n):遍历操作的时间复杂度,跟节点的个数 n 成正比
二叉查找树
左子树每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。

二叉树结构 (4)|330x275
查找
根节点-比之小> 左子树中递归查找。-比之大> 右子树中递归查找。
插入
从根节点开始比较,如果比之大,并且节点右子树为空,就将新数据插到右子节点;
如果不为空,就再递归遍历右子树,查找插入位置。
如果要插入的数据比节点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置。
如果不为空,就再递归遍历左子树,查找插入位置。

二叉树结构 (4)
删除
情况一:要删除的节点没有子节点
只需要直接将父节点中,指向要删除节点的指针置为 null。比如图中的删除节点 55。
情况二:如果要删除的节点只有一个子节点(只有左子节点或者右子节点),
只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中的删除节点 13。
情况三:如果要删除的节点有两个子节点
找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。
然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。

取巧方法:将要删除的节点标记为“已删除”。
重要特性
中序遍历二叉查找树,可以得到有序的数据序列,时间复杂度是 O(n)。因此,二叉查找树又称二叉排序树。
class BinarySearchTree:
def init(self) -> None:
self.tree = None

class Node:
    def __init__(self, data) -> None:
        self.data = data
        self.left = None
        self.right = None

def insert(self, value):
    # 如果是根结点,直接插入
    if self.tree is None:
        self.tree = self.Node(value)
        return

    p = self.tree
    while p is not None:
        if value > p.data:
            if p.right is None:
                p.right = self.Node(value)
                return
            p = p.right
        elif value < p.data:
            if p.left is None:
                p.left = self.Node(value)
                return
            p = p.left

def find(self, value):
    p = self.tree
    while p is not None:
        if value > p.data:
            p = p.right
        elif value < p.data:
            p = p.left
        else:
            return p
    return None

def delete(self, value):
    p = self.tree
    pp = None
    while p is not None and p.data != value:
        pp = p
        if value > p.data:
            p = p.right
        elif value < p.data:
            p = p.left

    if p is None:
        return

    if p.left is not None and p.right is not None:
        tmp_p = p.right
        tmp_pp = p
        # 找要删除结点的右子树中的最小值
        while tmp_p.left is not None:
            tmp_pp = tmp_p
            tmp_p = p.left
        p.data = tmp_p.data
        p = tmp_p
        pp = tmp_pp

    if p.left is not None:
        child = p.left
    elif p.right is not None:
        child = p.right
    else:
        child = None

    # 删除根结点
    if pp is None:
        self.tree = child
    elif pp.left is p:
        pp.left = child
    elif pp.right is p:
        pp.right = child

def pre_order(self, node):
    if node is None:
        return
    print(node.data)
    self.pre_order(node.left)
    self.pre_order(node.right)

def in_order(self, node):
    if node is None:
        return
    self.in_order(node.left)
    print(node.data)
    self.in_order(node.right)

def post_order(self, node):
    if node is None:
        return
    self.post_order(node.left)
    self.post_order(node.right)
    print(node.data)

def test_binary_search_tree():

binary_search_tree = BinarySearchTree()
data = [1, 10, 20, 40, 13]
for i in data:
    binary_search_tree.insert(i)
assert 20 == binary_search_tree.find(20).data
binary_search_tree.delete(20)
assert binary_search_tree.find(20) is None
# 1 10 40 13
binary_search_tree.pre_order(binary_search_tree.tree)
print("-----------------------")
# 1 10 13 40
binary_search_tree.in_order(binary_search_tree.tree)
print("-----------------------")
# 13 40 10 1
binary_search_tree.post_order(binary_search_tree.tree)

if name == ‘main’:
test_binary_search_tree()

3.冒泡排序-python
https://www.processon.com/diagraming/6063da99e0b34d392e5efc7c850×470 6.42 KB
https://www.processon.com/diagraming/6063da99e0b34d392e5efc7c

普通冒泡
j 的取值(j += 1):第一次冒泡:04第二次冒泡:03第三次冒泡:02第三次冒泡:01第五次冒泡:0
j 的上限可通过 i(从 0 ~ 数组长度的值) 和 n (数组的长度)进行控制:第一次冒泡:4第二次冒泡:3第三次冒泡:2第三次冒泡:1第五次冒泡:0
class Sort:
def bubble_sort(self, data):
n = len(data)
for i in range(0, n):
for j in range(0, n - i - 1):
if data[j] > data[j+1]:
tmp = data[j]
data[j] = data[j+1]
data[j+1] = tmp

def test_sort():
data = [3, 5, 4, 1, 2, 6]
a = Sort()
a.bubble_sort(data)
data2 = [1, 2, 3, 4, 5, 6]
assert data == data2

if name == ‘main’:
test_sort()

提前退出
利用 flag 提前退出
3, 5, 4, 1, 2, 6
class Sort:
def bubble_sort(self, data):
n = len(data)
flag = False
for i in range(0, n):
for j in range(0, n - i - 1):
if data[j] > data[j+1]:
tmp = data[j]
data[j] = data[j+1]
data[j+1] = tmp
flag = True
if not flag:
break

def test_sort():
data = [3, 5, 4, 1, 2, 6]
a = Sort()
a.bubble_sort(data)
data2 = [1, 2, 3, 4, 5, 6]
assert data == data2

if name == ‘main’:
test_sort()

时间复杂度
最好:O(n)
最坏:O(n²)
平均:O(n²)
空间复杂度
O(1)
稳定性
稳定:

6.插入排序-python
插入排序
有序度:比如说2、4、3、1、5、6这组数组的有序度是11,因为它有11个有序元素对,分别是(2,4)、(2,3)、(2,5) (2,6)、(4,5)、(4,6)、(3,5)、(3,6)、(1,5)、(1,6)、(5,6)。
逆序度:满有序度 - 有序度。
对于 6、5、4、3、2、1这组数据,它的有序度是0;对于1、2、3、4、5、6这组数据来说,它的有序度是n*(n-1)/2,这种完全有序的数组的有序度叫做满有序度。
移动次数 == 逆序度:n*(n-1)/2
class InsertionSort:
def sort(self, data):
for i in range(1, len(data)):
value = data[i]
j = i - 1
while j >= 0 and data[j] > value:
data[j + 1] = data[j]
j -= 1
data[j + 1] = value

if name == ‘main’:
insertion_sort = InsertionSort()
data = [5, 2, 1, 4, 10, 3, 6, 7]
insertion_sort.sort(data)
assert data == [1, 2, 3, 4, 5, 6, 7, 10]

空间复杂度:O(1)时间复杂度:最好:O(n)最坏:O(n²)平均:O(n*((1+2+3+…+n)/n)) = O(n²)
稳定:

7.选择排序-python
选择排序与插入排序类似。
维护有序序列:

选择排序

选择排序

class SelectionSort:
def sort(self, data):
length = len(data)
if length <= 1:
return
# 选择每一个元素
for i in range(length):
min_index = i
min_value = data[i]
# 找出后续最小元素的值和位置
for j in range(i, length):
if data[j] < min_value:
min_index = j
min_value = data[j]
# 交互位置
data[i], data[min_index] = data[min_index], data[i]

if name == ‘main’:
selection_sort = SelectionSort()
data = [5, 2, 1, 4, 10, 3, 6, 7]
selection_sort.sort(data)
assert data == [1, 2, 3, 4, 5, 6, 7, 10]

时间复杂度
最好:O(n²)
最坏:O(n²)
平均:O(n²)
空间复杂度O(1)
稳定

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值