数据结构与算法

499 阅读9分钟

时间复杂度和空间复杂度

一、时间复杂度

  • 如果运行时间是常数量级,则用常数1表示。
  • 只保留时间函数中的最高阶项。
  • 如果最高阶项存在,则省去最高阶项前面的系数。

场景1: T(n) = 3n = O(n),执行次数是线性的

def eat1(n):
    for i in range(n):
        print('吃1')
        print('吃2')
        print('吃3')

场景2: T(n) = 3logn = O(logn),执行次数是对数计算的

def eat2(n):
    while n > 1:
        print('吃1')
        print('吃2')
        print('吃3')
        n /= 2

场景3: T(n) = 3 = O(1),执行次数是常量

def eat3(n):
    print('吃1')
    print('吃2')
    print('吃3') 

场景4: T(n) = (n+1)*(n/2) = O(n²),执行次数是线性的

def eat4(n):
    for i in range(n):
        for j in range(i):
            print('吃1')
        print('吃2')

二、空间复杂度

1. 常量空间: S(n) = O(1),算法储存空间大小固定,和输入没有直接关系

def eat1(n):
    i = 3

2. 线性空间: S(n) = O(n),算法储存空间是线性集合,和n成正比

def eat2(n):
    i = [None] * n

3. 二维空间: S(n)=O(n²),算法储存空间是二维列表集合,且长宽和n成正比

def eat3(n):
    i = [[None] * n] * n

4. 递归空间: S(n) = O(n),递归操作所需要的储存空间和递归的深度成正比

def eat4(n):
    if n > 0:
        eat4(n-1)

数据结构

一、栈

栈是一种遵从 后进先出 原则的有序集合。新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端叫做栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。

二、队列

队列是一种遵循 先进先出 原则的一组有序的项。队列在尾部添加元素,并从顶部移除元素。最新添加的元素必须排在队列的末尾。

三、链表

链表储存有序的元素集合,但不同于数组,链表的每个元素由一个储存元素本身的节点和一个指向下一个元素的引用组成。

1. 单向链表

节点只有链向下一个节点的链接。 单向链表

class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None

2. 双向链表

节点的链接是双向的,一个链向下一个元素,另一个链向前一个元素。 双向链表

class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None
        self.prev = None

3. 循环链表

最后一个元素指向下一个元素的指针不是引用null,而是指向第一个元素head。 循环链表

4. 双向循环链表

有指向head元素的tail.next,和指向tail元素的head.prev。

双向链表

5. 数组和链表的区别

  1. 链表是随机的存储结构;数组是顺序的存储结构。
  2. 链表通过指针来连接元素与元素;数组则是把所有元素按次序依次存储。
  3. 链表的增、删元素相对数组较为简单;数组查找某个元素较为简单。

image.png

四、哈希表(HashMap)

以键-值形式储存元素,能够知道值的具体位置,因此能够快速检索到该值。

1. 冲突版

HashMap数组每一个元素的初始值都是Null。调用 hashMap.put("apple", 0),插入一个key为“apple”的元素。这时候我们需要利用一个哈希函数来确定Entry的插入位置:

index = Hash('apple')

假定最后计算出的index是2,那么结果如下:

hashmap

因为HashMap的长度是有限的,当插入的Entry越来越多时,再完美的Hash函数也难免会出现index冲突的情况。

2. 链表法

HashMap数组的每一个元素不止是一个Entry对象,也是一个链表的头节点。每一个Entry对象通过Next指针指向它的下一个Entry节点。当新来的Entry映射到冲突的数组位置时,只需要插入到对应的链表即可。

hashmap

3. 开放寻址法

当想向表中某个位置加入一个新元素的时候,如果索引为index的位置已经被占据了,就尝试index+1的位置。如果index+1的位置也被占据了,就尝试index+2的位置,以此类推。

五、二叉树

1. 常见二叉树

根节点,子树,叶子节点,深度4(高度、最大层数)

满二叉树:所有非叶子结点都存在左右孩子,并且所有叶子结点都在同一层级上(每一个分支都是满的)
完全二叉树:编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同(最后一个节点之前的节点都齐全)

用数组储存二叉树

(1) 二叉搜索树
二叉搜索树(Binary Search Tree)是二叉树的一种,但是它只允许你在左侧节点存储比父节点小的值,在右侧节点存储比父节点大或者等于父节点的值。

(2) 二叉平衡树
1 左子树和右子树都是AVL树。
2 左子树和右子树的高度差不能超过1 。
3 适合搜索

(3) 红黑树
红黑树是一种特化的AVL树
1 每个结点或者为黑色或者为红色
2 根结点为黑色
3 每个叶结点(实际上就是NULL指针)都是黑色的
4 如果一个结点是红色的,那么它的两个子节点都是黑色的(也就是说,不能有两个相邻的红色结点)
5 对于每个结点,从该结点到其所有子孙叶结点的路径中所包含的黑色结点数量必须相同
6 适合插入和删除

(4) 堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

在构造堆的基本思想就是:首先将每个叶子节点视为一个堆,再将每个叶子节点与其父节点一起构造成一个包含更多节点的对。 所以,在构造堆的时候,首先需要找到最后一个节点的父节点,从这个节点开始构造最大堆;直到该节点前面所有分支节点都处理完毕,这样最大堆就构造完毕了。

堆

2. 二叉树遍历

(1) 中序遍历:左根右

res = []
def dfs(root):
    if not root: return
    dfs(root.left)
    res.append(root)
    dfs(root.right)

def mid_order(root):
    if root == None:
        return
    res = []
    stack = []
    while root or len(stack) > 0:
        if root:
            stack.append(root)
            root = root.left
        else:
            root = stack.pop().val
            res.append(root.val)
            root = root.right
    return res

3-5-6-7-8-9-10-11-12-13-14-15-18-20-25

(2) 先序遍历:根左右

res = []
def dfs(root):
    if not root:return
    res.append(root)
    dfs(root.left)
    dfs(root.right)

def pre_order(root):
    if root == None:
        return []
    res = []
    stack = []
    while root or len(stack) > 0:
        if root:
            res.append(root.val)
            stack.append(root)
            root = root.left
        else:
            root = stack.pop().right
    return res

11-7-5-3-6-9-8-10-15-13-12-14-20-18-25

(3) 后序遍历:左右根

res = []
def dfs(root):
    if not root:return
    dfs(root.left)
    dfs(root.right)
    res.append(root)

def post_order(root):
    if root == None:
        return []
    res = []
    stack1 = [root]
    stack2 = []
    while stack1:
        node = stack1.pop()
        if node.left:
            stack1.append(node.left)
        if node.right:
            stack1.append(node.right)
        stack2.append(node)
    while stack2:
        res.append(stack2.pop().val)
    return res

3-6-5-8-10-9-7-12-14-13-18-25-20-15-11

(4) 层次遍历

def level_order(root):
    if root == None:
        return
    res = []
    stack = [root]
    while stack:
        node = stack.pop(0)
        res.append(node.val)
        if node.left:
            stack.append(node.left)
        if node.right:
            stack.append(node.right)

算法

一、排序算法

时间复杂度:算法执行的次数。
空间复杂度:算法消耗的内存空间。
稳定性:能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。

排序

1. 冒泡排序

(1) 比较相邻的两个元素,如果前一个比后一个大,则交换位置。
(2) 第一轮的时候最后一个元素应该是最大的一个。
(3) 按照步骤一的方法进行相邻两个元素的比较,这个时候由于最后一个元素已经是最大的了,所以最后一个元素不用比较。

优化
(1) 经过前几轮排序后已经有序,但依然在继续冒泡。可以用isSorted标识,如本轮未进行交换,则表示已有序,可以跳出循环。
(2) 有些区域一开始就有序,但还在反复冒泡。可以记录最后交换的位置,表示这一部分已经有序。

def bubble_sort(list):
    for i in range(len(list)-1,0,-1):
        for j in range(i):
            if list[j] > list[j+1]:
                list[j],list[j+1] = list[j+1],list[j]
    return list

2. 选择排序

(1) 在未排序序列中找到最小元素,存放到排序序列的起始位置。
(2) 再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。

def select_sort(list):
    length = len(list)
    for i in range(length):
        min = i
        for j in range(i,length):
            if list[j] < list[min]:
                min = j
        list[i],list[min] = list[min],list[i]
    return list

3. 插入排序

(1) 假设数列第一个元素为已排序数列,剩余数列为未排序。
(2) 将待排序元素挨个插入到已排序数列中每次插入都必须保证数列是有序的。
(3) 适合大部分已经有序,且总数较少的情况。

def insert_sort(list):
    length = len(list)
    for i in range(1,length):
        for j in range(i):
            if list[j] > list[j+1]:
                list[j],list[j+1] = list[j+1],list[j]
    return list

4. 希尔排序

(1) 将整个待排序的列分割成为若干子序列。
(2) 分别进行直接插入排序。
(3) 优化版的插入排序(使每个子序列有序),但当特殊情况,所有元素分在同一组时,时间复杂度依然是 O(n²),反而多了分组成本。

def shell_sort(list):
    gap = len(list) // 2
    while gap > 0:
        for i in range(gap, len(list)):
            for j in range(i, 0, -gap):
                if list[j] < list[j - gap]:
                    list[j], list[j - gap] = list[j - gap], list[j]
        gap = gap // 2
    return list

5. 归并排序

(1) 分成子序列,对子序列进行排序
(2) 将已有序的子序列合并,得到完全有序的序列

def mergeSort(arr):
    if len(arr) < 2:
        return arr
    middle = len(arr) // 2
    left = arr[:middle]
    right = arr[middle:]
    def merge(left, right):
        result = []
        while len(left) and len(right):
            if left[0] <= right[0]:
                result.append(left.pop(0))
            else:
                result.append(right.pop(0))
        if left: result.extend(left)
        if right: result.extend(right)
        return result
    return merge(mergeSort(left), mergeSort(right))

6. 快速排序

快速排序是对冒泡排序的一种改进,第一趟排序时将数据分成两部分,一部分比另一部分的所有数据都要小。然后递归调用,在两边都实行快速排序。

def quick_sort(list):
    if not list:
        return []
    else:
        first = list[0]
        left = quick_sort([l for l in list[1:]if l < first])
        right = quick_sort([l for l in list[1:] if l > first])
        return left +[first] + right

7. 堆排序

它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。

def heap_sort(array):
    def heap_adjust(parent):
        child = 2 * parent + 1  # left child
        while child < len(heap):
            if child + 1 < len(heap):
                if heap[child + 1] > heap[child]:
                    child += 1  # right child
            if heap[parent] >= heap[child]:
                break
            heap[parent], heap[child] = \
                heap[child], heap[parent]
            parent, child = child, 2 * child + 1

    heap, array = array.copy(), []
    for i in range(len(heap) // 2, -1, -1):
        heap_adjust(i)
    while len(heap) != 0:
        heap[0], heap[-1] = heap[-1], heap[0]
        array.insert(0, heap.pop())
        heap_adjust(0)
    return array

8. 计数排序

(1) 在n范围内,把数放在对应下标,然后整合。 (2) 适合已知范围内的整数排序

先假设20个随机整数的值是:9, 3, 5, 4, 9, 1, 2, 7, 8, 1, 3, 6, 5, 3, 4, 0, 10, 9, 7, 9

9. 桶排序

在n范围内划分几个区间,把数放在区间内,然后区间内排序,再整合。

二、查找算法

1. 顺序搜索

def sequentialSearch(arr, target):
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

2. 二分搜索

def search(nums: List[int], target: int) -> int:
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = left + (right - left) // 2
        if nums[mid] == target:
            return pivot
        if target < nums[pivot]:
            right = mid - 1
        else:
            left = mid + 1
    return -1