Loading

数据结构总结

本文是小猿圈 算法&数据结构 学习笔记

程序 = 数据结构 + 算法 —— Niklaus Wirth

简介

数据结构分为逻辑结构和物理结构。逻辑结构中,又分别分为:

	- 线性结构,各个元素成一对一关系,如列表
	- 树结构,各个元素成一对多关系,如堆
	- 图结构,各个元素成多对多关系,常见地图数据的存储是其一种应用实例

列表

数组

C语言中,类列表的数据结构被称之为数组,它相对于列表有两个特性:

	1. 元素类型固定
	2. 长度固定

因为数组占用的是一块整体连续内存,所以首先要明确元素类型,以确定每个元素占用的大小(比如在32位机器上,一个整数占用的内存大小为4个字节),其次确定数组的长度(元素个数),以确定整个数组的大小。
在存储数组列表时,解释器会记录数组的起始内存地址,当要对列表进行取值时,它的时间复杂度是O(1),原因如下:

	假设有一个长度为 8 的整数型数组 a
	且 a 的起始内存地址为 100
	如果要取 a[2],
	那么只需要使用 100 + 2 * 4 即可得到 a[2] 的值

列表

列表是Python中的一种基本数据结构,不同于数组的是,它的元素类型和长度都不是固定的。但它的取值时间复杂度同样为O(1),这是因为在Python中,列表并不是直接存储的数据,而是数据所在的内存地址(在32位机器上,内存地址占用的大小为4个字节,而64位机器上则为8个字节),所以能保证其存储的元素类型是固定的,只是取值时需要在数组基础上多一步寻址的操作。
而实际上列表长度也是固定的,之所以使用时感觉不是固定的,是因为Python解释器首先会取得一块固定大小的内存,每当添加元素时超出内存大小时,Python会创建一块新的内存,并将原来的数据复制到新的内存中(初始内存和新内存的大小是根据一定算法确定的),所以使用时是不固定的。

列表相关操作的时间复杂度
  1. 插入元素(insert)
    当使用 insert 方法为列表插入元素时,该元素位置后面的元素都要相对的往后移一位
    所以其时间复杂度为 O(n)
  1. 删除元素(remove)
    同插入元素
    因为涉及到改变列表中其它元素的位置
    所以其时间复杂度为 O(n)
    pop 方法不指定参数时除外,因为它总是弹出最后一位
  1. 添加元素(append)
    不考虑扩展内存大小的情况,它的时间复杂度为 O(1)

栈简介及代码实现

栈是一种数据集合,可以理解为只能在一端进行插入或删除操作的列表。栈具有"后入先出 LIFO"(last-in, first-out)的特点。

    进栈  
      |   / \
     \ /   |
          出栈
    |_________|  --> 栈顶
    |_________|
    |_________|
    |_________|  --> 栈底

根据栈的特点和基本操作,可以使用列表简单的实现栈:

class StackEmpty(Exception):
    pass


class Stack(object):
    
    def __init__(self):
        self._stack = []

    def push(self, element):
        """进栈/压栈"""
        self._stack.append(element)

    def pop(self):
        """出栈
        取出并删除栈顶"""
        self._assert_not_empty()
        return self._stack.pop()

    def get_top(self):
        """取栈顶
        取出但不删除栈顶
        """
        self._assert_not_empty()
        return self._stack[-1]

    def is_empty(self):
        return len(self._stack) == 0

    def _assert_not_empty(self):
        if not self._stack:
            raise StackEmpty()

栈的应用:括号匹配

假定有一个字符串中,存在着()[]{}等括号,现在需要使用代码来检测括号是否完整以及使用方法是否正确。

"""
引用前面写的 Stack 类
"""

# 定义匹配元素
MATCH = {
    ']': '[',
    ')': '(',
    '}': '{',
}

def brance_match(string):
    """检测 string 中的括号是否匹配
    以及用法是否正确
    返回 True 或 False

    方法:
        遍历字符串中的每一个字符
        如果该字符是左括号则进栈
        
        是右括号则让栈顶出栈(如果栈为空则错误)
        且让栈顶与右括号匹配
        如果不正确则返回 False

        遍历结束后
        如果栈不为空
        表示缺少右括号
        返回 Fasle
    """
    stack = Stack()
    left_brances = MATCH.values()
    
    for ch in string:
        if ch in left_brances:
            stack.push(ch)
        elif ch in MATCH:
            if stack.is_empty():
                return False
            if stack.pop() != MATCH[ch]:
                return False
    
    if not stack.is_empty():
        return False
    else:
        return True

队列

简介

队列是一种数据集合,它仅允许在一端进行插入,另一端进行删除。进行插入的一端称为队尾(rear),插入元素的动作被称为"进队"或"入队";进行删除的一端称为队头(front),进行删除的动作称为"出队"。队列具有"先进先出(FIFO First-in First-out)"的特性。


            ———————————————————————————————————————————————
  <——- 出队  A1(队头)  A2  A3  A4  A5  A6  A7  ...  An(队尾)   <——- 入队                       
            ———————————————————————————————————————————————

由于队列的特殊性,如果简单的用列表的pop(0)remove(q[0])进行出队的话,会造成出队的时间复杂度为O(n),使得效率降低。因此队列有一种实现方式被称之为"环形队列",即将队视为一个圆形,它的构造及出入队流程如下图所示:

队列的实现方式——环形队列

注意其中的队头(font)是不指向值的。

简单的说,就是将队头和队尾变为指针,每次出队时,将队头的指针向后移一位,并将对头指向的元素弹出;入队时,则将队尾的指针向后移一位,并将入队的元素存入队尾指针所在位置。当队头与队尾相等时,则队列为空;当队尾向后移一位等于队头时,则队满。

队列的实现

由于环形队列的特性,建队时需要明确队列的大小(maxsize)。且当队尾/队首指向首尾相接的地方时,向后移一位不能通过简单的加减法来实现。所以通用的做法是:对队列大小取模。使得当队尾/队头指针front == maxsize -1时,再前进一个位置就自动为0。而关于队尾/队头的每一步操作也需要根据maxsize来进行,具体如下:

    队首指针前进 1: front = (front + 1) % maxsize
    队尾指针前进 1: rear = (rear + 1) % maxsize
    对空条件: rear == front
    队满条件: (rear + 1) % maxsize == front

代码实现:

class QueueEmpty(Exception):
    pass


class QueueFilled(Exception):
    pass


class Queue(object):

    def __init__(self, maxsize):
        # 因需要留一个空位来保证正确判断是否队空
        # 所以实际上创建的队列要比 maxsize 大 1
        # 以保证使用时队列大小符合预期
        size = maxsize + 1
        self._queue = [0 for _ in range(size)]
        self.size = size
        self._front = 0
        self._rear = 0

    def is_empty(self):
        """是否队空"""
        return self._front == self._rear

    def is_filled(self):
        """是否队满"""
        return (self._rear + 1) % self.size == self._front

    def push(self, element):
        if self.is_filled():
            raise QueueFilled('Queue is filled.')
        self._rear = (self._rear + 1) % self.size
        self._queue[self._rear] = element

    def pop(self):
        if self.is_empty():
            raise QueueEmpty('Queue is empty.')
        self._front = (self._front + 1) % self.size
        return self._queue[self._front]

    def __str__(self):
        return f'<Queue {[self._queue[i] for i in range(self._front+1, self._rear+1)]}>'

此外,如果想要实现一个无限大的队列,那么只需要首先创建一个足够大的队列,当队满时,再创建一个新的更大的队列,并将原来的队列数据复制到新的队列中即可。

双向队列

双向队列两端都支持进队和出队,Python内置的deque模块实现了该队列。具体参考Python标准库 deque 用法。使用deque可以轻松实现linuxtail命令,因为一旦deque队满后(如果有指定队列的最大值),继续添加deque则会自动弹出前面的元素。具体实现如下:

"""
假定有 test.txt 文件位于本文件同级目录
内容为:
    sdsdedeffessds1
    sdsdedeffessds2
    sdsdedeffessds3
    sdsdedeffessds4
    sdsdedeffessds5
    sdsdedeffessds6
    sdsdedeffessds7
    sdsdedeffessds8
"""
from collections import deque


def tail(path, n):
    """给定文件和行数,返回该文件的最后 n 行数据
    :param path: 文件路径
    :param n: 显示最后的行数

    test:
        >>> tail('test.txt', 2)
        ['sdsdedeffessds7', 'sdsdedeffessds8']
    """
    with open(path, 'r') as f:
        # 注意 f 是一个可迭代对象
        # 每次迭代返回 f.readline()
        q = deque(f, n)
        return [line for line in q]

迷宫问题

迷宫问题是一个经典的关于广度优先和深度优先的问题,使用栈和队列可以分别实现其深度优先解决方案和广度优先解决方案。首先假定有一个二维数组,它代表一个迷宫,所有的数字1表示墙,而0则表示路,具体如下:

maze = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1],
    [1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1],
    [1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1],
    [1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1],
    [1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
    [1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1],
    [1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1],
    [1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1],
    [1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]

而迷宫问题则是:给定一个点的坐标作为起点,另一个点的坐标作为终点,求起点到终点的路径(如果有)?

使用栈解决迷宫问题——深度优先搜索

"""
主要思路:
    首先创建一个栈
    
    每当走一步时,将当前位置放入栈中
    并按照固定的顺序(如上、右、下、左)寻找可走的下一步
    同时对已经走过的路进行标记

    当找不到下一步可走时
    弹出栈顶(相当于回退到上一步)
    再次按固定顺序寻找下一步可走的路

    循环直到到达指定位置
    或栈为空时

    如果栈为空则表示无路可以到达指定地点
"""

# 寻找下一步的顺序
# 上、下、左、右
order_for_next_step = [
    lambda x, y: (x - 1, y),
    lambda x, y: (x + 1, y),
    lambda x, y: (x, y-1),
    lambda x, y: (x, y+1),
]

def maze_path(x1, y1, x2, y2):
    """给定起点和终点坐标,返回迷宫路径
    如果没有路径则返回 False
    :param x1: 起点横轴坐标
    :param y1: 起点纵轴坐标
    :param x2: 终点横轴坐标
    :param y2: 终点纵轴坐标
    :return: 路径或False

    test:
        >>> maze_path(1, 1, 11, 10)
        [(1, 1), (2, 1), (3, 1), (4, 1), 
        (5, 1), (6, 1), (6, 2), (6, 3), 
        (6, 4), (6, 5), (6, 6), 
        (7, 6), (7, 7), (6, 7), 
        (6, 8), (7, 8), (8, 8), (8, 9), 
        (9, 9), (10, 9), (11, 9), (11, 10)]
    """
    stack = []
    stack.append((x1, y1,))
    while stack:
        # 获取当前所在位置
        cur_x, cur_y = stack[-1]

        # 如果当前路径等于目标路径
        # 则表示已经到达了终点
        # stack 记录的是所有走过的路径
        if (cur_x, cur_y,) == (x2, y2,):
            return stack

        # 找到下一步要走的位置
        for nex_step in order_for_next_step:
            x, y = nex_step(cur_x, cur_y)

            # 如果找到下一步要走的位置
            # 将下一步坐标添加到栈中
            # 并将当前位置标记为 2 表示已走过
            if maze[x][y] == 0:
                stack.append((x, y,))
                maze[x][y] = 2
                break
        else:
            # 如果找不到下一步要走的位置 则回退
            stack.pop()
    else:
        # 如果 stack 为空
        # 表示没有正确的路径能到达终点
        return False

需要注意的是,使用栈虽然可以找出两个点的路径,但往往并不是最优路径,存在着许多不必要的路。而使用队列则可以很好的解决这个问题。

使用队列解决迷宫问题——广度优先搜索

使用队列完成迷宫问题,可以找到两个点的最优可行距离。主要思路是用队列记录相对于当前点的每一个可行的点,并弹出当前点,同时用一个列表当前点的坐标以及当前点的上一个节点在该列表中的位置(用于追朔点的路径)。然后依次遍历记录好的可行的点,直到到达终点。具体代码如下:

from collections import deque


def maze_path_queue(x1, y1, x2, y2):
    q = deque()
    # 首先添加当前点位置及来源位置
    # 无来源定为 -1
    q.append((x1, y1, -1,))
    path = []

    # 循环直到队列为空为止
    while len(q) != 0:
        cur_x, cur_y, pos = q.popleft()
        path.append((cur_x, cur_y, pos,))

        # 将当前点记标记为已走
        maze[cur_x][cur_y] = 2

        if cur_x == x2 and cur_y == y2:
            return find_path(path)

        # 遍历上下左右四个点
        # 如果为可走的点
        # 则将该点加入到队列中
        # 并且记录来源点在 path 中的位置
        # 该位置总是当前 path 的最后一个
        for step in order_for_next_step:
            next_x, next_y = step(cur_x, cur_y)
            if maze[next_x][next_y] == 0:
                q.append((next_x, next_y, len(path)-1,))
    else:
        return False


def find_path(path):
    """根据path中记录的点及位置找到路径"""
    real_path = []
    i = len(path) - 1
    while i >= 0:
        x, y, pos = path[i]
        real_path.append((x, y,))
        i = pos

    # 最后的列表是倒着记录的
    # 所以需要反转
    real_path.reverse()
    return real_path

链表

简介

链表是一种类似于列表的数据结构,不同于列表/数组的是,它的每一个点都有一个指针指向下一个点。比如:

class Node(object):
    """用该对象封装链表的每一个元素
    使得每一个元素都包含两个区域:
        1. 数据存放区
        2. 下一个元素的存放区
    """
    def __init__(self, item):
        self.item = item
        self.next = None
>>> c = Node(1)
>>> b = Node(2, c)
>>> a = Node(3, b)
>>> a.next.next.item
1

链表的实现

  1. 头插法。头插法是相对于尾插法来讲的,无论头尾都能实现一个链表,只不过正常输入的顺序相反(相对于列表来讲,一般头插法是倒序,尾插法是顺序)。不妨先看一下头插法的实现。我们可以编写一个方法轻易的将一个有序列表变为链表:
def make_link_list(li):
    """将一个列表转换为单向链表,返回链表头部
    :param li: 列表
    :return: 链表
    """
    head = Node(li[0])
    for n in li[1:]:
        node = Node(n)
        node.next = head
        head = node
    return head
  1. 输出链表。输出链表也至关重要。但仍然简单,只需要不断的调next就可以了:
def print_linklist(lk):
    while lk:
        print(lk.item)
        lk = lk.next
  1. 至此尾插法也可以实现了:
def make_link_list_tail(li):
    """根据列表创建链表之尾插法"""
    # 初始化时头尾指向一个元素
    head = Node(li[0])
    tail = head
    for n in li[1:]:
        node = Node(n)
        tail.next = node
        tail = node
    # 最后仍返回 head
    return head

通过尾插法构造的链表同样可以调用print_linklist来实现输出,试一下就知道,同样的方法,它和头插法输出的顺序是相反的。

链表的插入、删除操作

链表的插入和删除时间复杂度都是O(1),它不需要像列表一样每次插入和删除都需要移动其它元素的位置,而只需要改变左右元素的指向即可。具体如下:

# 以下操作皆相对于 current_node

# 插入元素 p:
>>> p.next = current_node.next
>>> current_node.next = p

# 删除元素 p:
>>> p = current_node.next
>>> current_node.next = p.next
>>> del p    

双链表

双链表表示单个元素同时保存前后两个元素的信息。实现如下:

class Node(object):

    def __init__(self, item):
        self.item = item
        self.next = None
        self.prior = None

双链表的插入与删除时间复杂度同样为O(1),具体如下:

# 插入元素 p:
>>> p.next = current_node.next
>>> current_node.next.prior = p
>>> p.prior = current_node
>>> current_node.next = p

# 删除元素 p:
>>> p = current_node.next
>>> current_node.next = p.next
>>> p.next.prior = current_node
>>> del p

哈希表

简介

哈希表是一种通过哈希函数来计算数据存储位置的数据结构。通常支持如下操作(时间复杂度为O(1)):

  1. insert(key, [value]): 插入键值对;
  2. get(key): 如果存在key则返回对应的value,否则返回None
  3. delete(key): 删除键值对

直接寻址表

哈希表也是一种线性存储结构,它的概念要先从直接寻址表说起。直接寻址表是指先创建一个所有可能存在的元素的列表(要求所有元素必须为数字,每个元素对应该列表的下标),每当有新的元素进来时,就找到该元素对应的下标,再将该元素存储到对应的下标位置上;当删除该元素时,也先找到下标后直接删除即可,查找同理。直接寻址表的缺点是当元素很多时,需要创建的列表也很大。同时如果实际元素较少而列表较大,则容易大量空间被浪费。最后,它也无法处理元素不是数字的情况。

哈希表

哈希的出现改善了直接寻址表的部分缺点。它会首先构建一个大小为m的寻址表T,每当有新元素进来时,通过一个算法函数h,这个函数接收参数k,通过该算法(h(k))会得到一个T的下标,从而快速的实现插入、存储和查找等操作。这就是哈希表,它由一个直接寻址表(T)和一个哈希函数(h)组成。

哈希冲突及其解决办法

哈希表虽然一定程度上解决了直接寻址表存在的缺点,但它本身也有缺点。由于哈希表的大小是有限的,而要存储的值的数量是无限的,因此对于任何哈希函数,都会出现两个不同元素映射到同一个位置的情况,这种情况就叫做哈希冲突。比如:

    h(k) = k % 7
    h(0) = h(7) = h(14) ...

哈希冲突常见的解决方案有:

  1. 开放寻址法。如果哈希函数返回的位置已经有值,则可以向后探查新的位置来存储这个值。常见方法有:
- 线性探查
    如果位置 i 被占用,则探查 i+1 i+2 ...

- 二次探查
    如果位置 i 被占用,则探查 i+1方 i-1方 i+2方 i-2方 ...

- 二度哈希
    保存 n 个哈希函数,当使用第一个哈希函数找到的地址出现冲突时,则往后调用下一个哈希函数算出新的地址
  1. 拉链法。哈希表的每一个位置不再直接存储元素,而是存储一个包含元素的链表,每当有新元素进来且出现哈希冲突时,则将该元素插入到对应链表中。

哈希表的简单实现(拉链法)

class LinkNode(object):
    """链表节点"""
    def __init__(self, item):
        self.item = item
        self.next = None

    def __repr__(self):
        return f'<LinkNode {self.item}>'


class LinkList(object):
    """链表"""
    def __init__(self, iterator=None):
        self.head = None
        self.tail = None
        if iterator is not None:
            self.extend(iterator)

    def append(self, item):
        node = LinkNode(item)
        if self.head is None:
            self.head = node
            self.tail = node
        else:
            self.tail.next = node
            self.tail = node

    def extend(self, iterator):
        for item in iterator:
            self.append(item)

    def find(self, item):
        for n in self:
            if n.item == item:
                return True
        return False

    def delete(self, item):
        for n in self:
            # 第一个元素
            if n.item == item:
                self.head = n.next
                return

            if n.next is None:
                raise IndexError('Item not existed.')

            if n.next.item == item:
                n.next = n.next.next
                return

    def __iter__(self):
        tmp = self.head
        while tmp:
            yield tmp
            tmp = tmp.next

    def __repr__(self):
        return f'<LinkList [{",".join(str(n.item) for n in self)}]>'


class HashTable(object):
    """哈希表
    基于链表结构和取模算法
    该哈希表元素仅支持整数"""
    def __init__(self, size=100):
        self.size = size
        self._li = [LinkList() for _ in range(size)]

    def _h(self, item):
        """哈希方法
        返回 item 在 self._li 中的索引位置"""
        return item % self.size

    def insert(self, item):
        index = self._h(item)
        self._li[index].append(item)

    def get(self, item):
        index = self._h(item)
        return self._li[index].find(item)

    def delete(self, item):
        index = self._h(item)
        self._li[index].delete(item)

    def __repr__(self):
        return f'<HashTable {self._li}>'

哈希表的应用

字典与集合

字典与集合都是通过哈希表来实现的,以含键值对的字典来说,它通过哈希函数将键映射为下标,而该下标对应的位置则存储键对应的值。如果发生哈希冲突,则通过拉链法或开放寻址法解决。

md5算法

MD5(Message-Digest Algorithm 5)曾经是密码学中常用的哈希函数(现已被破解),它可以把任意长度的数据映射为128位的哈希值。MD5含如下特征:

  1. 同样的消息,其md5值必定相同
  2. 可以快速计算出任意给定消息的md5
  3. 除非暴力枚举所有可能的消息,否则不可能从哈希值反推出消息本身
  4. 两条消息之间即使只有微小的差别,其对应的md5值也是完全不同,也不相关的
  5. 不能在有意义的时间内人工的构造两个不同的消息,使其具有相同的md5

虽然MD5已经被破解,单它在很多领域仍然发挥着重要的作用,比如对文件的哈希值:算出文件的哈希值,如果两个文件的哈希值是相同的,则可以认为这两个文件是相同的。基于此:

1. 用户可以用它来判断下载的文件是否完整
2. 云服务商可以用它来判断用户要上传的文件是否已经存在于服务器上,从而实现秒传的功能
SHA2算法

历史上的MD5SHA-1曾经是使用最为广泛的加密哈希函数,但随着密码学的发展,这两个哈希函数的安全性相继收到了各种挑战。因此现在安全性比较重要的场合推荐使用SHA-2等新的更安全的哈希函数。SHA-2MD5具有相似的特征。

树也是一种数据结构。它是可以递归定义的。它是由n个节点组成的集合。如果n0则表示一颗空数;如果n>0,那么则存在一个节点作为树的根节点,其它节点可以分为m个集合,每个集合本身又是一棵树。

树的简单实现与应用

计算机中常见的文档系统就是树的典形实现与应用。简单实现如下:

# 定义节点类型
DIR = 'DIR'
FILE = 'FILE'

class FileTreeNode(object):
    """文档树节点"""
    def __init__(self, name, node_type=DIR, parent=None):
        self.name = name
        self.node_type = node_type
        self.children = []
        self.parent = parent

    def __repr__(self):
        return f'{self.node_type} {self.name}'


class FileTree(object):
    """文档树"""
    def __init__(self):
        # 根目录
        self.root = FileTreeNode('/')
        # 当前所在目录
        self.now = self.root

    def _parse_name(self, name) -> list:
        """解析传入的名字中是否含多层级"""
        if '/' in name:
            return name.split('/')
        return [name]

    def mkdir(self, name):
        """创建文件夹"""
        d = FileTreeNode(name, parent=self.now)
        self.now.children.append(d)

    def ls(self):
        """列出当前目录下的所有文件和文件夹"""
        for c in self.now.children:
            print(c)

    def cd(self, name):
        """进入某个文件夹
        允许传入多层目录,以 / 分隔
        特别的:
            . 代表当前所在位置
            .. 代表上级所在位置
        """
        names = self._parse_name(name)
        children = None

        def flush_children():
            nonlocal children
            children = {n.name: n for n in self.now.children}

        flush_children()

        for n in names:
            if n == '.':
                continue
            elif n == '..':
                if self.now.parent is not None:
                    self.now = self.now.parent
                    flush_children()
                continue

            if n in children:
                self.now = children[n]
                flush_children()
            else:
                raise IndexError('Dir not existed.')

    def rm(self, name):
        """删除文件或文件夹"""
        if name == self.now.name:
            if self.now.parent is not None:
                self.now = self.now.parent
                return
            else:
                raise Warning('You are attempt to remove the root node.')
        for n in self.now.children:
            if n.name == name:
                self.now.children.remove(n)
                return
        else:
            raise IndexError('Dir not existed.')

    def touch(self, name):
        n = FileTreeNode(name, node_type=FILE)
        self.now.children.append(n)

def test():
    f = FileTree()
    f.mkdir('var')
    f.mkdir('usr')
    f.mkdir('home')
    f.cd('home')
    f.mkdir('test')
    f.mkdir('financial')
    f.mkdir('john')
    f.touch('n.txt')
    f.cd('../home')
    f.rm('john')
    f.ls()


if __name__ == '__main__':
    test()

# 输出
DIR test
DIR financial
FILE n.txt

二叉树

概念

二叉树(Binary Tree)是一种特殊的树结构,满足每个节点最多只有2个子节点。它的节点的代码形式如下:

class BiTreeNode(object):
    """二叉树节点
    :param data: 节点存储的数据
    """
    def __init__(self, data):
        self.data = data
        # 左孩子节点
        self.lchild = None
        # 右孩子节点
        self.rchild = None

    def __repr__(self):
        return f'<BiTreeNode {self.data}>'

二叉树的简单示例:

            E
         /     \
        A       G
      /   \      \
     B     C      F
          /
         D

二叉树的遍历

二叉树的遍历共有四种方法:

    1. 前序遍历
        递归从节点的左孩子节点开始遍历
    2. 中序遍历
        以节点为中点,递归从左往右依次进行遍历
    3. 后序遍历
        递归从节点的右孩子节点开始遍历
    4. 层次遍历
        每一层从左到右进行遍历,遍历完后继续下一层

实现:

def printf(data):
    """打印二叉树的节点
    用于遍历的时候"""
    print(data, end=',')


def pre_order(root):
    """前序遍历"""
    if root:
        printf(root.data)
        pre_order(root.lchild)
        pre_order(root.rchild)


def in_order(root):
    """中序遍历"""
    if root:
        in_order(root.lchild)
        printf(root.data)
        in_order(root.rchild)


def pos_order(root):
    """后续遍历"""
    if root:
        pos_order(root.lchild)
        pos_order(root.rchild)
        printf(root.data)


def level_order(root):
    """层次遍历
    思路:
        创建一个队列
        并将根节点放入队列中

        当队列不为空时
        弹出根节点
        并将根节点的左右节点依次入队(如果存在)
    """
    from collections import deque

    q = deque()
    q.append(root)

    while len(q) > 0:
        node = q.popleft()
        printf(node.data)

        if node.rchild:
            q.append(node.rchild)
        if node.lchild:
            q.append(node.lchild)

测试:

def create_test_bitree():
    """
    按照下面的结构创建一个二叉树示例,
    并返回其根节点

            E
         /     \
        A       G
      /   \      \
     B     C      F
          /
         D
    """
    a = BiTreeNode('A')
    b = BiTreeNode('B')
    c = BiTreeNode('C')
    d = BiTreeNode('D')
    e = BiTreeNode('E')
    f = BiTreeNode('F')
    g = BiTreeNode('G')

    e.lchild = a
    e.rchild = g
    a.lchild = b
    a.rchild = c
    c.lchild = d
    g.rchild = f

    return e


if __name__ == '__main__':
    root = create_test_bitree()
    pos_order(root)

# 输出
B,D,C,A,F,G,E,

附:根据二叉树遍历结果还原二叉树

若知道二叉树的前序遍历结果和中序遍历结果,则可以据此还原二叉树。知道后序和中序也可,但单知道一种或只知后序和前序则不行,因为其结果有多种可能。下面以知道后序和中序为前提还原二叉树,说一说大概思路:

设结果为:
    后序:BDCAFGE
    中序:BADCEGF

根据后序结果可知道最后一个肯定是根节点,即 E
找出 E 在中序的位置,则 E 前面的肯定属于根的左子树,即 BADC
E后面的属于根的右子树,即 GF

根据后序找到根左子树部分(BDCA),最后一个则是左子树的根节点,即 A
找到 A 在中序中的位置,则 A 前面的属于 A 的左子树,即 B,到此左子树已还原完成
A 后面的则为 A 的右子树,即 DC

根据后序找到A的右子树部分(DC),最后一个为A的右子树根节点,即 C
找到 C 在中序中的位置,C 前面的属于 C 的左子树,即 D,到此右子树还原完成

根节点 E 的右子树照此类推

二叉搜索树

概念

二叉搜索树是一颗二叉树且满足如下性质:

设 x 是二叉树的一个节点

    - 若 y 为 x 的左子树的一个节点,那么 y.key <= x.key
    - 若 y 为 x 的右子树的一个节点,那么 y.key >= x.key

用大白话说就是:对二叉树的任意节点,它的左子树的任意节点的值都比它本身的值小,它的右子树的任意节点的值都比它本身的值大,那么这颗二叉树就可以被叫做二叉搜索树。示例:

            6
         /     \
        3       8
      /   \      \
     2     5      9
          /
         4

二叉搜索树的操作:

    - 查询
    - 插入
    - 删除

二叉搜索树实现

"""
二叉搜索树
"""
class BSTNode(object):
    """树节点"""
    def __init__(self, data):
        self.data = data
        # 左孩子节点
        self.lchild = None
        # 右孩子节点
        self.rchild = None
        # 父节点
        self.parent = None

    def __repr__(self):
        return f'<BSTNode {self.data}>'


class BST(object):

    def __init__(self, iterator=None):
        self.root = None
        if iterator is not None:
            for val in iterator:
                # self.insert_rec(val, self.root)
                self.insert(val)

    def insert(self, val):
        """非递归插入"""
        if not self.root:
            self.root = BSTNode(val)
            return

        p = self.root
        while True:
            # 小于时分两种情况:
            # 1. 有右孩子节点 -> 往下找
            # 2. 无右孩子节点 -> 赋值并返回
            if p.data < val:
                if p.rchild:
                    p = p.rchild
                else:
                    p.rchild = BSTNode(val)
                    p.rchild.parent = p
                    return

            # 与上面同理
            elif p.data > val:
                if p.lchild:
                    p = p.lchild
                else:
                    p.lchild = BSTNode(val)
                    p.lchild.parent = p
                    return

            # 相等的情况暂不考虑
            else:
                return

    def insert_rec(self, val, node):
        """递归插入"""
        if not self.root:
            self.root = BSTNode(val)
            return

        if not node:
            node =  BSTNode(val)

        # 如果插入的值比当前节点的值大
        # 则看向当前节点的右节点
        elif node.data < val:
            node.rchild = self.insert_rec(val, node.rchild)
            node.rchild.parent = node

        # 与上面同理
        elif node.data > val:
            node.lchild = self.insert_rec(val, node.lchild)
            node.lchild.parent = node

        # 当与当前节点的值相等时
        # 暂时不做考虑
        # 如果需要考虑存储相同值的情况
        # 可以考虑往 BSTNode 添加新的属性 count
        # 每当插入已存在的值时
        # 将该节点的 count 加 1 即可
        else:
            pass

        return node

    def query_rec(self, val, node):
        """递归查找"""
        if not self.root:
            return
        if node.data < val:
            return self.query_rec(val, node.rchild)
        elif node.data > val:
            return self.query_rec(val, node.lchild)
        else:
            return node

    def query(self, val):
        """非递归查找"""
        if not self.root:
            return

        p = self.root
        while p:
            if p.data < val:
                p = p.rchild
            elif p.data > val:
                p = p.lchild
            else:
                return p
        else:
            return None

    def order(self, root=None, mode='in'):
        if root is None:
            root = self.root
        self._order(root, mode)

    def _order(self, root, mode='in'):
        """遍历
        :param mode:
            in: 中序遍历
            pre: 前序遍历
            post: 后序遍历
        """
        if mode == 'in' and root:
            self._order(root.lchild, mode)
            print(root.data, end=',')
            self._order(root.rchild, mode)

        elif mode == 'pre' and root:
            print(root.data, end=',')
            self._order(root.lchild, mode)
            self._order(root.rchild, mode)

        elif mode == 'post' and root:
            self._order(root.lchild, mode)
            self._order(root.rchild, mode)
            print(root.data, end=',')

    def remove(self, val):
        """
        移除节点,分为下面几种情况:

        1. 节点不存在
            抛出错误

        2. 根为空,表示空树
            抛出错误

        3. node 的父节点为 None
            表示 node 为根节点
            考虑4种情况,具体做法参考情况4-6:
            3.1. node 只有一个左孩子节点
            3.2. node 只有一个右孩子节点
            3.3. node 有两个孩子节点
            3.4. node 没有孩子节点(叶子节点)

        4. node 为树的叶子节点:
            4.1. node 为左孩子节点
                将 node 的父节点的左孩子节点置为 None
            4.2. node 为右孩子节点
                将 node 的父节点的右孩子节点置为 None

        5. node 有一个孩子节点
            5.1. node 有左孩子节点
                5.1.1. node 是 node 父节点的左孩子节点
                    示例图:
                                node.parent
                                /
                            node          ...
                            /
                        node.lchild

                    将 node 的父节点的左孩子节点置为 node 的左孩子节点
                    将 node 的左孩子节点的父节点置为 node 的父节点

                5.1.2. node 是 node 父节点 的右孩子节点
                    示例图:
                            node.parent
                            /        \
                          ...        node
                                    /
                                node.lchild

                    将 node 的父节点的右孩子置为 node 的左孩子节点
                    将 node 的左孩子节点的父节点置为 node 的父节点

            5.2. node 有右孩子节点
                5.2.1. node 是 node 父节点的左孩子节点
                    示例图:
                                node.parent
                                /
                            node          ...
                                \
                            node.lchild

                    将 node 的父节点的左孩子节点置为 node 的右孩子节点
                    将 node 的右孩子节点的父节点置为 node 的父节点

                5.2.2. node 是 node 父节点 的右孩子节点
                    示例图:
                            node.parent
                            /        \
                          ...        node
                                        \
                                    node.lchild

                    将 node 的父节点的右孩子置为 node 的右孩子节点
                    将 node 的右孩子节点的父节点置为 node 的父节点

        6. node 有两个孩子节点
            示例图:
                        root
                      /      \
                     A        B
                   /   \    /   \
                node    D  E     F
                 / \
                G   H
               ... / \
                  I   J
                   \
                    K

            从 node 的右孩子节点向下挨个找左孩子节点
            直到找到最后一个为止,假设为示例图中的 I

            此时分 2 种情况:
            6.1. I 是叶子节点
                将 I 节点放到 node 节点的位置上
                删除 node 节点
            6.2. I 有且只有一个右孩子节点
                将 I 的右孩子节点放到 I 节点的位置上
                将 I 节点放到 node 节点的位置上
                (参考情况 5.2.1)
        """
        node = self.query(val)

        # 节点不存在 或 空树
        if not node or not self.root:
            raise IndexError('Try to remove a not existed node.')

        # 节点为叶子节点
        if not node.lchild and not node.rchild:
            self._remove_leaf(node)

        # 有且只有一个右孩子节点
        elif not node.lchild:
            self._remove_with_rchild(node)

        # 有且只有一个左孩子节点
        elif not node.rchild:
            self._remove_with_lchild(node)

        # 有两个孩子节点
        else:
            min_node = node.rchild
            while min_node.lchild:
                min_node = min_node.lchild

            node.data = min_node.data
            # min_node 有且只有一个右孩子节点
            if min_node.rchild:
                self._remove_with_rchild(min_node)
            else:
                self._remove_leaf(min_node)

    def _remove_leaf(self, node):
        # node 为叶子节点
        parent = node.parent
        if not node.lchild and not node.rchild:
            if not parent:
                self.root = None
            elif parent.lchild == node:
               parent.lchild = None
            else:
                parent.rchild = None

    def _remove_with_rchild(self, node):
        # node 只有一个右孩子节点
        parent = node.parent
        rchild = node.rchild
        if not parent:
            self.root = rchild
            return
        if parent.lchild == node:
            parent.lchild = rchild
        else:
            parent.rchild = rchild
        rchild.parent = parent

    def _remove_with_lchild(self, node):
        # node 只有一个左孩子节点
        parent = node.parent
        lchild = node.lchild
        if not parent:
            self.root = lchild
            return
        if parent.lchild == node:
            parent.lchild = lchild
        else:
            parent.rchild = lchild
        lchild.parent = parent


def test():
    """
            4
        2       6
     1     3  5    7
                       9
                    8

    """
    import random

    li = [i for i in range(500)]
    random.shuffle(li)

    t = BST([4,6,7,9,2,1,3,5,8])
    t.order(mode='pre')
    print('')
    # print(t.query_rec(7, t.root))
    print(t.query(7))
    print(t.query(4))
    t.remove(4)
    t.remove(7)
    t.order(mode='pre')



if __name__ == '__main__':
    test()

AVL树

AVL树是一颗自旋转的二叉搜索树。实现略。

posted @ 2021-07-22 13:17  kingron  阅读(118)  评论(0)    收藏  举报