数据结构与算法入门系列(1) - 快速了解数据结构与算法

数据结构与算法入门系列(1) - 快速了解数据结构与算法

数据结构

分类

按物理结构分为

  1. 顺序存储结构
  2. 链式存储结构

按逻辑结构分为

  1. 集合结构
  2. 线性结构:数组、单链表、队列、栈等
  3. 树形结构
  4. 图形结构

常用数据结构

数组

在连续的内存空间中,存储一组相同类型的元素

  • 访问/索引 O ( 1 ) O(1) O(1)
  • 搜索元素 O ( n ) O(n) O(n)
  • 插入元素 O ( n ) O(n) O(n)
  • 删除元素 O ( n ) O(n) O(n)

特点:适合频繁访问,不适合频繁插入和删除,读多写少

# 1. 创建数组,python中用List即可,python的List允许放不同类型的元素
a = []

# 2. 添加元素
## 末尾添加元素 O(1)
a.append(1)
a.append(2)
a.append(3)
## 指定位置插入元素 O(n)
a.insert(2,99)
### a = [1,2,99,3]

# 3. 访问元素 O(1)
a[2]
## 99

# 4. 更新元素 O(1)
a[2] = 88

# 5. 删除元素
## a.remove(元素)
a.remove(88) # O(n)
## a.pop(索引)
a.pop(1) # O(n)
## 删除最后一个元素
a.pop() # O(1)

# 6. 获取数组长度
size = len(a)

# 7. 遍历数组 O(n)
## 第一种方法
for item in a:
    print(item)
## 第二种方法
for index,element in enumerate(a):
    print("Index at ", index," is: ", element)
## 第三种方法
for i in range(len(a)):
    print(a[i])

# 8. 查找某个元素 O(n)
## 想查找的元素的索引 = a.index(元素) 
index = a.index(2)

# 9. 数组排序 基于比较的排序,最好只能到O(nlogn)
## 默认升序
a = [3,1,2]
a.sort()
print(a)
## 降序排序
a.sort(reverse=True)
print(a)
链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的

  • 访问/索引 O ( n ) O(n) O(n)
  • 搜索元素 O ( n ) O(n) O(n)
  • 插入元素 O ( 1 ) O(1) O(1)
  • 删除元素 O ( 1 ) O(1) O(1)

特点:适合频繁插入和删除,不适合频繁访问,读少写多

# 1. 创建链表
# 2. 添加元素
# 3. 访问元素
# 4. 查找元素
# 5. 删除元素
# 6. 链表的长度
class Node:
    def __init__(self,item):
        self.item = item
        self.next = None

class SingleLinkList:
    def __init__(self):
        self._head = None
    def is_empty(self):
        return self._head is None
    def length(self):
        cur = self._head
        count = 0
        while cur is not None:
			count += 1
            cur = cur.next
        return count
    def items(self):
		cur = self._head
        while cur is not None:
			yield cur.item
            cur = cur.next
    def add(self,item):
        node = Node(item)
        node.next = self._head
        self._head = node
    def append(self,item):
        node = Node(item)
        if self.is_empty():
            self._head = node
        else:
            cur = self._head
            while cur.next is not None:
                cur = cur.next
            cur.next = node
    def insert(self,index,item):
        if index <= 0:
            self.add(item)
        elif index > (self.length()-1):
            self.append(item)
        else:
            node = Node(item)
            cur = self._head
            for i in range(index-1):
                cur = cur.next
            node.next = cur.next
            cur.next = node
    def remove(self,item):
        cur = self._head
        pre = None
        while cur is not None:
            if cur.item == item:
                if not pre:
                    self._head = cur.next
                else:
                    pre.next = cur.next
                return
            else:
                pre = cur
                cur = cur.next
    def find(self,item):
        return item in self.items()

链表包括单链表,双向链表,循环链表等,一般常用的是单链表

队列

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头

  • 访问/索引 O ( n ) O(n) O(n)(访问队头元素是 O ( 1 ) O(1) O(1))
  • 搜索元素 O ( n ) O(n) O(n)
  • 插入元素 O ( 1 ) O(1) O(1)
  • 删除元素 O ( 1 ) O(1) O(1)

特点:先进先出

from collections import deque
# 1. 创建队列 deque()是双端队列,使用时若只需要单端队列,则只用其单端就行了
queue = deque()
# 2. 添加元素 O(1)
queue.append(1)
queue.append(2)
queue.append(3)
# 3. 获取即将出队的元素 O(1)
temp1 = queue[0]
print(temp1)
# 4. 删除即将出队的元素 O(1)
## popleft在删除时同时可以返回删除的元素
temp2 = queue.popleft()
print(temp2)
## [2,3]
print(queue)
# 5. 判断队列是否为空 O(1) 因为len在内部的实现是:新来一个元素len+1,所以是随着元素的增删实时变化的
len(queue) == 0
# 6. 队列长度 O(1) 
len(queue)
# 7. 遍历队列(边删除边遍历队列操作) O(n)
while len(queue) != 0:
    temp = queue.popleft()
    ## print(temp)替换为相应的其他遍历时想执行的操作即可
    print(temp)

队列包括单端队列和双端队列等,一般常用的是单端队列

栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素

  • 访问/索引 O ( n ) O(n) O(n)(访问栈顶元素是 O ( 1 ) O(1) O(1))
  • 搜索元素 O ( n ) O(n) O(n)
  • 插入元素 O ( 1 ) O(1) O(1)
  • 删除元素 O ( 1 ) O(1) O(1)

特点:先进后出

# 1. 栈的创建
## 可以用列表实现栈,只用它的尾部添加和尾部删除即可
stack = []
# 2. 添加元素 O(1)
stack.append(1)
stack.append(2)
stack.append(3)
# 3. 查看栈顶元素---即将出栈的元素 O(1)
stack[-1]
# 4. 删除栈顶元素---即将出栈的元素 O(1)
## pop()删除栈顶元素的同时还可以返回删除的元素
temp = stack.pop()
print(temp)
# 5. 栈的长度 O(1) 因为len在内部的实现是:新来一个元素len+1,所以是随着元素的增删实时变化的
len(stack)
# 6. 栈是否为空 O(1)
len(stack) == 0
# 7. 遍历栈(边删除栈顶元素,边遍历) O(n)
while len(stack) > 0:
    temp = stack.pop()
    ## print(temp)替换为相应的其他遍历时想执行的操作即可
    print(temp)
哈希表

哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列/哈希函数,存放记录的数组叫做散列/哈希表

哈希碰撞/冲突:两个不同的key通过同一个哈希函数得到相同的值

解决办法:再散列、链地址/拉链法(冲突的地方用一个链表叠起来,访问到该位置时再遍历链表找到想要的key对于的key和value组合)等

  • 根据key查找元素 O ( 1 ) O(1) O(1) (碰撞时 O ( k ) , k O(k),k O(k),k是碰撞元素的个数)
  • 插入元素 O ( 1 ) O(1) O(1)(碰撞时 O ( k ) , k O(k),k O(k),k是碰撞元素的个数)
  • 删除元素 O ( 1 ) O(1) O(1) (碰撞时 O ( k ) , k O(k),k O(k),k是碰撞元素的个数)

特点:查找元素时间复杂度 O ( 1 ) O(1) O(1)

# 1. 创建哈希表
## 数组其实也算是一种哈希表,不过key是0到n-1
mapping = {}

# 2. 添加元素 O(1)
mapping[1] = 'hanmeimei'
mapping[2] = 'lihua'
mapping[3] = 'siyangyuan'

# 3. 删除元素 O(1)
## mapping.pop(key)删除key对应的key,value pair
mapping.pop(2)
# 或者用del mapping[2]

# 4. 修改元素 O(1)
mapping[1] = 'limeimei'
# 5. 获取元素
mapping[1]
# 6. 检查key是否存在 O(1)
## key in dict
3 in mapping
# 7. 哈希表长度 O(1)
len(mapping)
# 8. 哈希表是否还有元素 O(1)
len(mapping) == 0
集合

集合是指具有某种特定性质的具体的或抽象的对象汇总而成的集体,满足确定性、互异性、无序性

  • 搜索元素 O ( 1 ) O(1) O(1)(碰撞时 O ( k ) , k O(k),k O(k),k是碰撞元素的个数)
  • 插入元素 O ( 1 ) O(1) O(1)(碰撞时 O ( k ) , k O(k),k O(k),k是碰撞元素的个数)
  • 删除元素 O ( 1 ) O(1) O(1)(碰撞时 O ( k ) , k O(k),k O(k),k是碰撞元素的个数)

特点:无序性、互异性(不重复)

# 1. 创建集合
s = set()
# 2. 添加元素 O(1)
s.add(10)
s.add(3)
s.add(5)
s.add(2)
s.add(2)
## 此时可能是{2,10,3,5},但顺序其实是不确定的,或者说应该根据底层哈希算法确定,而我们不得而知
print(s)

# 3. 搜索元素 O(1)
2 in s
# 4. 删除元素 O(1)
s.remove(2)
# 5. 长度 O(1)
len(s)

集合包括hashset、linklistset、treeset等,常用的是hashset

树是由根结点和若干颗子树构成的。树是由一个集合以及在该集合上定义的一种关系构成的。集合中的元素称为树的结点,所定义的关系称为父子关系。父子关系在树的结点之间建立了一个层次结构。在这种层次结构中有一个结点具有特殊的地位,这个结点称为该树的根结点,或称为树根

最常见的树是二叉树,特殊的二叉树有满二叉树、完全二叉树(堆是一种特殊的完全二叉树,分为大顶堆(堆中某个结点的值总是不大于其父结点的值)和小顶堆(堆中某个结点的值总是不小于其父结点的值))、赫夫曼树、二叉搜索树、平衡二叉搜索树、红黑树、B树、B+树等

二叉树的遍历:

  1. 深度优先遍历DFS:
    • 前序:根节点->左子树->右子树
    • 中序:左子树->根节点->右子树
    • 后序:左子树->右子树->根节点
  2. 广度优先遍历BFS,即先第一层,再第二层,…

特点:1个前驱多个后继

from collections import deque
# 1. 二叉树的创建
class BinaryTree:
    def __init__(self, value):
        self.value = value
        self.left_child = None
        self.right_child = None
    ## insert_right可类似定义
    def insert_left(self, value):
        if self.left_child == None:
            self.left_child = BinaryTree(value)
        else:
            new_node = BinaryTree(value)
            new_node.left_child = self.left_child
            self.left_child = new_node
    ## 前序遍历
    def pre_order(self):
        print(self.value)

        if self.left_child:
            self.left_child.pre_order()

        if self.right_child:
            self.right_child.pre_order()
    ## 中序遍历
    def in_order(self):
        if self.left_child:
            self.left_child.in_order()

        print(self.value)

        if self.right_child:
            self.right_child.in_order()
    ## 后序遍历
    def post_order(self):
        if self.left_child:
            self.left_child.post_order()

        if self.right_child:
            self.right_child.post_order()
        
        print(self.value)
    ## BFS 广度优先
    def bfs(self):
        queue = deque()
        queue.append(self)

        while not queue.empty():
            current_node = queue.popleft()
            print(current_node.value)

            if current_node.left_child:
                queue.append(current_node.left_child)

            if current_node.right_child:
                queue.append(current_node.right_child)
        
# 2. 一般的树的创建 也可以类似定义前序、中序、后序遍历和BFS
class Tree:
    def __init__(self, value):
        self.value = value
		self.child = []

堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。

将根结点最大的堆叫做最大堆或大根堆或大顶堆,根结点最小的堆叫做最小堆或小根堆或小顶堆

  • 访问堆顶元素 O ( 1 ) O(1) O(1)
  • 插入元素 O ( log ⁡ n ) O(\log n) O(logn)(插入后调整以维持堆的特性,需要至多 log ⁡ n \log n logn次比较和交换)
  • 删除堆顶元素 O ( log ⁡ n ) O(\log n) O(logn)(删除堆顶元素后调整以维持堆的特性,需要至多 log ⁡ n \log n logn次比较和交换)
  • 数组堆化 O ( n ) O(n) O(n) (堆排序中建堆过程时间复杂度O(n)怎么来的?)

特点:完全二叉树、某个结点的值总是不大于或不小于其父结点的值

import heapq
# 1. 创建堆
minheap = []
heapq.heapify(minheap) ## 数组堆化,最小堆
# 2. 添加元素
heapq.heappush(minheap,10)
heapq.heappush(minheap,8)
heapq.heappush(minheap,9)
heapq.heappush(minheap,2)
heapq.heappush(minheap,1)
heapq.heappush(minheap,11)
## 此时堆为[1,2,9,10,8,11]
print(minheap)

# 3. 获取堆顶元素
print(minheap[0])
# 4. 删除堆顶元素
## heapq.heappop(minheap)在删除堆顶元素的同时还可以返回删除的元素
temp = heapq.heappop(minheap)
# 5. 堆的长度
len(minheap)
# 6. 堆的遍历(边删除边遍历操作)
while len(minheap) != 0:
    s = heapq.heappop(minheap)
    ## print(s)可以替换为其他想在遍历过程中执行的操作
    print(s)

# 7. 要想实现最大堆,需要首先将数组中全部元素取相反数
## 比如[1,2,3]要实现最大堆,先变为[-1,-2,-3]实现它的最小堆
## 以后每次遍历使用时再取反,比如拿到-3,再取反就等价于拿到3
并查集

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。

并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。

在计算机科学中,一个图就是一些顶点的集合,这些顶点通过一系列结对(连接)。顶点用圆圈表示,边就是这些圆圈之间的连线

图包括无向图、有向图、权重图(最短路径问题)等

图的遍历:深度优先DFS、广度优先BFS

树和图都是常常结合算法一起考查,具体看后面相关算法部分

算法

算法复杂度分析

时间复杂度

算法的执行时间与问题的规模或者说算法的输入值的规模 n n n之间的关系,一般用大 O O O表示法表示,表明渐进上界,时间复杂度一般考虑最坏情形下的时间复杂度,有时也考虑平均情形下的时间复杂度。另外还有大 Θ \Theta Θ和大 Ω \Omega Ω表示法,分别表示渐进紧确界和渐进下界

常见时间复杂度有: O ( 1 ) , O ( n ) , O ( log ⁡ n ) , O ( n log ⁡ n ) , O ( n 2 ) O(1),O(n),O(\log n),O(n\log n),O(n^2) O(1),O(n),O(logn),O(nlogn),O(n2)

空间复杂度

算法在实现时所需的辅助存储空间,即不考虑输入数据所占空间,只考虑算法实现所需的额外空间,一般也用大O表示法表示

常见空间复杂度有: O ( 1 ) , O ( n ) , O ( n 2 ) O(1),O(n),O(n^2) O(1),O(n),O(n2)

分析空间复杂度的方法:

  1. 看算法设计时创建的变量,如果变量赋值是常量,则一般是 O ( 1 ) O(1) O(1),赋值一维数组,一般是 O ( n ) O(n) O(n),赋值二维数组,一般是 O ( n 2 ) O(n^2) O(n2),以此类推
  2. 看算法是否用了递归,用递归则会把每层的临时信息都放入递归栈,若调用 n n n次,则需要额外 n n n乘以每层的空间的辅助空间

时间复杂度和空间复杂度之间往往是不可兼得的,需要根据实际情况取舍

常用算法技巧

双指针

双指针即为两个指针解决一道题,此外更复杂的还可以用更多的指针,这里主要讨论双指针

双指针主要分为

  • 普通双指针:两个指针往同一个方向移动
  • 对撞双指针:两个指针相向移动 (适用于有序数组两数之和等情形)
  • 快慢双指针:慢指针+快指针(适用于检测环形链表等情形)

相关题目:Leetcode141、Leetcode881

二分查找

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列

相关题目:Leetcode704、Leetcode35、Leetcode162、Leetcode74

滑动窗口

滑动窗口,就是一个滑动的窗口,套在一个序列中,左右的滑动,窗口内就是一个内容集

滑动窗口的应用场景有几个特点:

  1. 需要输出或比较的结果在原数据结构中是连续排列的

  2. 每次窗口滑动时,只需观察窗口两端元素的变化,无论窗口多长,每次只操作两个头尾元素,当用到的窗口比较长时,可以显著减少操作次数(减少while循环),适用于数组中定长问题等情形

  3. 窗口内元素的整体性比较强,窗口滑动可以只通过操作头尾两个位置的变化实现,但对比结果时往往要用到窗口中所有元素

相关题目:Leetcode209、Leetcode1456

递归

程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回

递归的4个要素

  • 接受的参数
  • 返回值
  • 终止的条件
  • 递归拆解:如何递归下一层
# 递归:先递进,再回归——这就是「递归」
f(6)
=> 6 * f(5)
=> 6 * (5 * f(4))
=> 6 * (5 * (4 * f(3)))
=> 6 * (5 * (4 * (3 * f(2))))
=> 6 * (5 * (4 * (3 * (2 * f(1)))))
=> 6 * (5 * (4 * (3 * (2 * 1))))
=> 6 * (5 * (4 * (3 * 2)))
=> 6 * (5 * (4 * 6))
=> 6 * (5 * 24)
=> 6 * 120
=> 720 

相关题目:Leetcode509、Leetcode206、Leetcode344

分治

分治,字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。在计算机科学中,分治法就是运用分治思想的一种很重要的算法。分治法是很多高效算法的基础,如排序算法(快速排序,归并排序)等等

采用分治法解决的问题一般具有的特征如下:

  1. 问题的规模缩小到一定的规模就可以较容易地解决

  2. 问题可以分解为若干个规模较小的模式相同的子问题,即该问题具有最优子结构性质

  3. 合并问题分解出的子问题的解可以得到问题的解

  4. 问题所分解出的各个子问题之间是独立的,即子问题之间不存在公共的子问题

特点:大问题分解成小问题(divide),解决小问题再合并(conquer),用到了递归(自己调用自己)

相关题目:Leetcode169、Leetcode53

回溯

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称,它也用到递归的思想,不严格地说可以把回溯理解为DFS+剪枝

回溯算法也叫试探法,它是一种系统地搜索问题的解的方法,用回溯算法解决问题的一般步骤:

  1. 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解

  2. 确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间

  3. 以深度优先(一般是深度优先,有时也可以采用别的方式遍历)的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索

相关题目:Leetcode22、Leetcode78

深度优先DFS

深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次,往往也会用到递归的思想

DFS主要应用:二叉树的DFS遍历、图的DFS遍历

相关题目:Leetcode938、Leetcode200

广度优先BFS

宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止,BFS往往结合队列来实现

BFS主要应用:二叉树的BFS遍历、图的BFS遍历

相关题目:Leetcode102、Leetcode107

并查集

并查集(Union-Find Set)是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示

并查集主要包含以下两个操作

  • Union: 合并两个元素为同一个根节点
  • Find: 找到某个元素的根节点
# 不加优化的UnionFind
class UnionFind:
	def __init__(self,n):
		self.root = list(range(n))
    def find(self,x):
		if x == self.root[x]:
            return self.root[x]
        else:
            return self.find(self.root[x])
    def union(self,x,y):
        rootX = self.find(x)
        rootY = self.find(y)
        if rootX != rootY:
			self.root[rootX] = rootY
            
# 加优化
class UnionFind:
	def __init__(self,n):
		self.root = list(range(n))
        ## 用于union保持较低的树高度,减少union后find时查询次数
        self.rank = [0]*n
    def find(self,x):
		if x == self.root[x]:
            return self.root[x]
        else:
            ## 加quick find优化,思想:查过一次就记录下来,以后不必再层层重复查,直接就可以找到根节点
            self.root[x] = self.find(self.root[x])
            return self.root[x]
    def union(self,x,y):
        rootX = self.find(x)
        rootY = self.find(y)
        ## 根据rank合并,保持较低的树高度
        if rootX != rootY:
            if self.rank[rootX] > self.rank[rootY]:
				self.root[rootY] = rootX
            elif self.rank[rootX] < self.rank[rootY]:
                self.root[rootX] = rootY
            else:
                self.root[rootY] = rootX
                self.rank[rootX] += 1

相关题目:Leetcode200、Leetcode547

贪心

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。思想:每一步得到局部最优解,希望最后能得到全局最优解,至于最后能不能得到全局最优解,有的问题可以,有的问题不行

相关题目:Leetcode1217、Leetcode55

记忆化搜索

记忆化算法在求解的时候每求解一个状态,就将它的解保存下来,以后再次遇到这个状态的时候,就不必重新求解了,也成为备忘录,其目的是减少重复计算,比如递归计算斐波那契数列时就可以每次存下来计算过的斐波那契数,这是一种空间换时间的技巧

相关题目:Leetcode509、Leetcode322

动态规划

动态规划(英语:Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划不是某一种具体的算法,而是一种算法思想:若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。应用这种算法思想解决问题的可行性,对子问题与原问题的关系,以及子问题之间的关系这两方面有一些要求,它们分别对应了最优子结构和重复子问题

特点:动态规划的关键在于定义合适的dp数组(可以是1维的、2维的甚至多维的)及理解其含义,以及找到状态转移方程

动态规划常用于以下问题:

  • 计数:如机器人从左上角到右下角多少个路径
  • 求最值:如机器人从左到右路径的最大数字和
  • 求存在性:如是否存在机器人从左到右的路径

相关题目:Leetcode509、Leetcode62、Leetcode121、Leetcode70、Leetcode279、Leetcode221

前缀树

字典树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高

如下图是b,abc,abd,bcd,abcd,efg,hii这7个单词的trie树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xl7y0ttc-1617526046543)(imgs/trie.png)]

# python3实现的前缀树的初始化、插入、搜索、startswith操作
from collections import deque
class Trie:

    def __init__(self):
        """
        Initialize your data structure here.
        """
        self.children = {}
        self.isEnd = False
        self.val = ""

    def insert(self, word: str) -> None:
        """
        Inserts a word into the trie.
        """
        cur = self
        for c in word:
            if c in cur.children:
                cur = cur.children[c]
            else:
                newNode = Trie()
                cur.children[c] = newNode
                cur = newNode
        cur.val = word
        cur.isEnd = True


    def search(self, word: str) -> bool:
        """
        Returns if the word is in the trie.
        """
        cur = self
        for c in word:
            if c in cur.children:
                cur = cur.children[c]
            else:
                return False
        return True if cur.isEnd else False


    def startsWith(self, prefix: str) -> bool:
        """
        Returns if there is any word in the trie that starts with the given prefix.
        """
        cur = self
        for c in prefix:
            if c in cur.children:
                cur = cur.children[c]
            else:
                return False
        queue = deque()
        queue.append(cur)
        while len(queue) > 0:
            temp = queue.popleft()
            if temp.isEnd: return True
            for key in temp.children:
                queue.append(temp.children[key])
        return False



# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)

相关题目:Leetcode208、Leetcode720、Leetcode692

Leetcode720. 给出一个字符串数组 words 组成的一本英语词典。从中找出最长的一个单词,该单词是由 words 词典中其他单词逐步添加一个字母组成。若其中有多个可行的答案,则返回答案中字典序最小的单词。

若无答案,则返回空字符串。

示例 1:

输入:
words = ["w","wo","wor","worl", "world"]
输出:"world"
解释: 
单词"world"可由"w", "wo", "wor", 和 "worl"添加一个字母组成。

示例 2:

输入:
words = ["a", "banana", "app", "appl", "ap", "apply", "apple"]
输出:"apple"
解释:
"apply"和"apple"都能由词典中的单词组成。但是"apple"的字典序小于"apply"。

提示:

  • 所有输入的字符串都只包含小写字母。
  • words 数组长度范围为 [1,1000]
  • words[i] 的长度范围为 [1,30]

标签
字典树 哈希表

解答

class Trie:
    def __init__(self):
        self.children = {}
        self.isEnd = False
        self.val = ""
class Solution:
    def longestWord(self, words: List[str]) -> str:
        if words is None or len(words) == 0: return ""
        root = Trie()
        for word in words:
            cur = root
            for c in word:
                if c in cur.children:
                    cur = cur.children[c]
                else:
                    newNode = Trie()
                    cur.children[c] = newNode
                    cur = newNode
            cur.val = word
            cur.isEnd = True
        
        result = ""
        for word in words:
            cur = root
            if len(word) > len(result) or (len(word) == len(result) and word < result):
                isWord = True
                for c in word:
                    cur = cur.children[c]
                    if not cur.isEnd:
                        isWord = False
                        break
                result = word if isWord else result
        return result

参考书目

  1. 《大话数据结构》
  2. 《算法图解》
  3. 《算法导论》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值