Python-数据结构与算法(十一、字典(映射)——基于两种不同的底层实现)

保证一周更两篇吧,以此来督促自己好好的学习!代码的很多地方我都给予了详细的解释,帮助理解。好了,干就完了~加油!
声明:本python数据结构与算法是imooc上liuyubobobo老师java数据结构的python改写,并添加了一些自己的理解和新的东西,liuyubobobo老师真的是一位很棒的老师!超级喜欢他~
如有错误,还请小伙伴们不吝指出,一起学习~
No fears, No distractions.

一、字典简述

  1. 字典(也叫映射)
  2. 结构特点:存储(键,值)数据对的数据结构(Key,Value)。根据键(Key),来寻找值(Value)
  3. 应用:花名册(学号 -> 人)、车辆管理(车牌号 -> 车)、数据库(id -> 信息)、词频统计(单词 -> 频率)
  4. 字典容易使用二分搜索树或者链表来实现,比如用二分搜索树来实现,相应的更改一下Node包含的元素就好了,比如:
    用二分搜索树实现时:
# 这里为了更清楚的展示Node类,用C++来写的。
class Node{
    K key;
    V value;
    Node* left;
    Node* right
}

用链表实现时:

class Node{
    K key;
    V value;
    Node* next;
}
  1. 字典接口:
    void add(K, V) 添加元素
    V remove(K) 删除元素(Key充当索引的作用)
    bool contains(K) 是否包含键值为K的元素
    V get(K) 获取键值为K的Value
    int getSize() 获取字典的大小
    bool isEmpty() 判断字典是否为空
  2. 基于二分搜索树的字典是有顺序性的,而基于链表实现的字典是无顺序性的,但是一般不用链表来实现无顺序性的字典,而用哈希表,哈希表后面会讲到。

二、基于二分搜索树的集合

1. 实现

# -*- coding: utf-8 -*-
# Author:           Annihilation7
# Data:             2018-10-20  10:30am
# Python version:   3.6

from queue.loopqueue import LoopQueue  # 会用到我们以前实现的循环队列

class Node:
    def __init__(self, key=None, value=None):
        """
        Description: 节点类的构造函数
        """
        self.key = key          # 键
        self.value = value      # value
        self.left = None        # 指向左孩子的标签
        self.right = None       # 指向右孩子的标签

class BstDict:
    def __init__(self):
        """
        Description: 基于二分搜索树的字典类的构造函数
        """
        self._root = None       # 初始化根节点为None 
        self._size = 0          # 初始化self._size

    def getSize(self):
        """
        Description: 获取字典内有效元素的个数
        Returns:
        有效元素的个数
        """
        return self._size       

    def isEmpty(self):
        """
        Description: 判断字典是否为空
        Returns:
        bool值,空为True
        """
        return self._size == 0

    def add(self, key, value):
        """
        Description: 向字典中添加键-值对,若键已经存在字典中,那就更新这个键所对应的value
        时间复杂度:O(logn)
        Params:
        - key: 待添加的键
        -value: 待添加的键所对应的value
        """
        self._root = self._add(self._root, key, value)   # 调用self._add方法,常规套路

    def contains(self, key):
        """
        Description: 查看字典的键中是否包含key
        时间复杂度:O(logn)
        Params:
        - key: 待查询的键值
        Returns:
        bool值,存在为True
        """
        return self._getNode(self._root, key) is not None # 调用self._getNode私有函数,看返回值是否是None

    def get(self, key):
        """
        Description: 得到字典中键为key的value值
        时间复杂度:O(logn)
        Params:
        - key: 待查询的键值
        Returns:
        相应的value值。若key不存在,就返回None
        """
        node = self._getNode(self._root, key)  # 拿到键为key的Node
        if node:                               # 如果该Node不是None
            return node.value                  # 返回对应的value
        else:
            return None                        # 否则(此时不存在携带key的Node)返回None

    def set(self, key, new_value):
        """
        Description: 将字典中键为key的Node的value设为new_value。注意,为防止与add函数发生混淆,
        此函数默认用户已经确信key在字典中,否则报错。并不会有什么新建Node的操作,因为这么做为与add函数有相同的功能,就没有意义了。
        时间复杂度:O(logn)
        Params:
        - key: 将要被设定的Node的键
        - new_value: 新的value值
        """
        node = self._getNode(self._root, key)
        if node is None:
            raise Exception('%s doesn\'t exist!' % key)   #报错就完事了
        node.value = new_value

    def remove(self, key):
        """
        Description: 将字典中键为key的Node删除。注:若不存在携带key的Node,返回Node就好。这个remove函数和前面的二分搜索树的remove函数极为相似,不理解的可以去前面看一下
        时间复杂度:O(logn)
        Params:
        - key: 待删除的键
        Returns:
        被删除节点的value
        """
        ret = self._minimum(self._root)             # 调用self._minimum函数保存携带最小值的节点,便于返回
        self._root = self._remove(self._root, key)  # 调用self._remove函数
        return ret.value                            # 返回携带最小值节点的value

    def printBstDict(self):
        """对字典进行打印操作,这里使用广度优先遍历,因为比较直观"""
        if self._root is None:
            return 
        queue = LoopQueue()
        queue.enqueue(self._root)
        while not queue.isEmpty():
            node = queue.dequeue()
            print('[Key: %s, Value: %s]' % (node.key, node.value))
            if node.left:
                queue.enqueue(node.left)
            if node.right:
                queue.enqueue(node.right) 

    # private method
    def _add(self, node, key, value):
        """
        Description: 向以node为根的二叉搜索树中添加键-值对
        Params:
        - node: 根节点
        - key: 待添加的键
        -value: 待添加的键所对应的value
        Returns:
        添加后新的根节点
        """
        getNode = self._getNode(self._root, key)     # 先判断字典中是否存在携带这个键的Node
        if getNode is not None:           # 如果已经存在
            getNode.value = value         # 将这个节点的value设为新传入的value就好
            return node                   # 返回根节点,不需要维护self._size哦

        if node is None:                  # 递归到底的情况,该添加节点啦
            self._size += 1              # 维护self._size
            return Node(key, value)       # 将新节点返回
        
        if key < node.key:                # 待添加key小于node的key,向左子树继续前进。这里的操作在二叉搜索树中详细讲过,就不BB了
            node.left = self._add(node.left, key, value)
        elif key > node.key:              # 待添加key大于node的key,向右子树继续前进。
            node.right = self._add(node.right, key, value)
        return node                       # 将node返回,这样在递归的回归过程中逐级返回,最终返回的是根节点

    def _remove(self, node, key):
        """
        Description: 将以node为根节点的字典中键为key的Node删除。注:若不存在携带key的Node,返回Node就好。
        时间复杂度:O(logn)
        Params:
        - node: 以node为根节点的字典
        - key: 待删除的节点所携带的键
        Returns:
        删除操作后的根节点
        """
        if node is None:    # 到头了都没找到key
            return None     # 直接返回None
        
        if key < node.key:  # 待删除key小于当前Node的key
            node.left = self._remove(node.left, key)    # 往左子树递归
            return node 
        elif node.key < key:    # 待删除key大于当前Node的key
            node.right = self._remove(node.right, key)  # 网右子树递归
            return node 
        else:               # 此时待删除key和当前Node的key相等,别忘了有三种情况
            if node.left is None:       # 左子树为空的情况       
                right_node = node.right # 记录当前Node的右孩子
                node.right = None       # 当前Node的右孩子设为None,便于垃圾回收
                self._size -= 1         # 维护self._size
                return right_node       # 将被删除Node的右孩子返回,即用右孩子来代替被删除节点的位置
            elif node.right is None:    # 右子树为空的情况,大同小异,不再赘述
                left_node = node.left 
                node.left = None 
                self._size -= 1
                return left_node
            else:   # 这里不再赘述了,不清楚的话回去看看二分搜索树那章,一样的操作
                successor = self._minimum(node.right)             
                successor.right = self._removeMin(node.right)  
                self._size += 1
                successor.left = node.left    
                node.left = node.right = None 
                self._size -= 1
                return successor

    def _minimum(self, node):
        """
        Description: 返回以node为根的二分搜索树的携带最小值的节点,下面的这些操作在二分搜索树中均有涉及,如果看不懂回到那一章再理解下就好了
        Params:
        - node: 根节点
        Returns:
        携带最小值的节点
        """
        if node.left is None:        # 递归到底的情况,node的左孩子已经为空了
            return node              # 返回node
            
        return self._minimum(node.left)   # 否则往左子树继续撸

    def _removeMin(self, node):
        """
        Description: 删除以node为根的二分搜索树的携带最小值的节点,同理,返回删除后的根节点
        Params:
        - node: 根节点
        Returns:
        返回删除操作后的树的根节点
        """
        if node.left is None:                # 递归到底的情况,node的左孩子已经为空 
            right_node = node.right          # 记录node的右孩子,为None也无所谓的
            node.right = None                # 将node的右孩子设为None,使其与树断绝联系,从而被垃圾回收
            self._size -= 1                  # 维护self._size
            return right_node                # 返回node的右孩子,即用右孩子来代替当前的node
        
        node.left = self._removeMin(node.left)    # 否则往node的左子树继续撸
        return node                          # 返回node,从而最终返回根节点


    def _getNode(self, node, key):
        """
        Description: 设置一个根据key来找到以node为根的二叉搜索树的相对应的Node的私有函数,便于方便的实现其他函数
        Params:
        - node: 传入的根节点
        - key: 带查找的key
        Returns:
        携带key的节点Node, 若不存在携带key的节点,返回None
        """
        if node is None:            # 都到底了还没有找到,直接返回None
            return None
        
        # 注意我们的二分搜索树依旧不包含重复的键哦~这也是字典的基本特点
        if key < node.key:          # 待查找的key小于当前节点的key,向左子树递归就完事了
            return self._getNode(node.left, key)
        elif node.key < key:        # 待查找的key大于当前节点的key,向右子树递归就完事了
            return self._getNode(node.right, key)
        else:                       # 此时带查找的key和node.key是相等的,直接返回这个Node就好
            return node 

2. 测试

from DICT.bstdict import BstDict  # 我们的基于二分搜索树的字典类写在bstdict.py文件中

test_bst = BstDict()
print('初始Size: ', test_bst.getSize())
print('是否为空?', test_bst.isEmpty())

add_list = [15, 4, 25, 22, 3, 19, 23, 7, 28, 24]
print('待添加元素:', add_list)
for add_elem in add_list:
    test_bst.add(add_elem, str(add_elem) + 'x')

    ##################################################
    #                  15(15x)                       #
    #               /           \                    #
    #            4(4x)          25(x)                #
    #            /    \       /       \              #
    #         3(3x)   7(7x) 22(22x)  28(28x)         #
    #                        /   \                   #
    #                    19(19x)  23(23x)            #
    #                               \                #
    #                                24(24x)         #
    ##################################################

print('添加元素后打印集合:')
test_bst.printBstDict()
print('此时的Size: ', test_bst.getSize())
print('字典中是否包含键23?', test_bst.contains(23))
print('获取键23的value:', test_bst.get(23))
print('将键为7的value设为"努力学习"并打印:')
test_bst.set(7, '努力学习')
test_bst.printBstDict()
print('删除键22并打印:')
test_bst.remove(22)
test_bst.printBstDict()
print('此时的Size:', test_bst.getSize())

3. 输出

初始Size:  0
是否为空? True
待添加元素: [15, 4, 25, 22, 3, 19, 23, 7, 28, 24]
添加元素后打印集合:
[Key: 15, Value: 15x]
[Key: 4, Value: 4x]
[Key: 25, Value: 25x]
[Key: 3, Value: 3x]
[Key: 7, Value: 7x]
[Key: 22, Value: 22x]
[Key: 28, Value: 28x]
[Key: 19, Value: 19x]
[Key: 23, Value: 23x]
[Key: 24, Value: 24x]
此时的Size:  10
字典中是否包含键23True
获取键23的value: 23x
将键为7的value设为"努力e学习"并打印:
[Key: 15, Value: 15x]
[Key: 4, Value: 4x]
[Key: 25, Value: 25x]
[Key: 3, Value: 3x]
[Key: 7, Value: 努力学习]
[Key: 22, Value: 22x]
[Key: 28, Value: 28x]
[Key: 19, Value: 19x]
[Key: 23, Value: 23x]
[Key: 24, Value: 24x]
删除键22并打印:
[Key: 15, Value: 15x]
[Key: 4, Value: 4x]
[Key: 25, Value: 25x]
[Key: 3, Value: 3x]
[Key: 7, Value: 努力学习]
[Key: 23, Value: 23x]
[Key: 28, Value: 28x]
[Key: 19, Value: 19x]
[Key: 24, Value: 24x]
此时的Size: 9

三、基于链表的字典

1. 实现

# -*- coding: utf-8 -*-
# Author:           Annihilation7
# Data:             2018-10-19   7:29 pm
# Python version:   3.6

class Node:
    def __init__(self, key=None, value=None, next=None):
        """
        Description: 节点的构造函数
        Params:
        - key: 传入的键值,默认为None
        - value: 传入的键所对应的value值,默认为None
        - next: 指向下一个Node的标签,默认为None
        """
        self.key = key     
        self.value = value 
        self.next = next      # 下一个节点为None


class LinkedListDict:
    """以链表作为底层数据结构的字典类"""
    def __init__(self):
        """
        Description: 字典的构造函数
        """
        self._dummyhead = Node()  # 建立一个虚拟头结点,前面讲过,不再赘述
        self._size = 0            # 字典中有效元素的个数

    def getSize(self):
        """
        Description: 获取字典中有效元素的个数
        Returns:
        有效元素的个数
        """
        return self._size 

    def isEmpty(self):
        """
        Description: 判断字典是否为空
        Returns:
        bool值,空为True
        """
        return self._size == 0

    def contains(self, key):
        """
        Description: 查看字典的键中是否包含key
        时间复杂度:O(n)
        Params:
        - key: 待查询的键值
        Returns:
        bool值,存在为True
        """
        return self._getNode(key) is not None   # 调用self._getNode私有函数,看返回值是否是None

    def get(self, key):
        """
        Description: 得到字典中键为key的value值
        时间复杂度:O(n)
        Params:
        - key: 待查询的键值
        Returns:
        相应的value值。若key不存在,就返回None
        """
        node = self._getNode(key)   # 拿到键为key的Node
        if node:                    # 如果该Node不是None
            return node.value       # 返回对应的value
        else:
            return None             # 否则(此时不存在携带key的Node)返回None

    def add(self, key, value):
        """
        Description: 向字典中添加key,value键值对。若字典中已经存在相同的key,更新其value,否咋在头部添加Node,因为时间复杂度为O(1)
        时间复杂度:O(n)
        Params:
        - key: 待添加的键值
        - value: 待添加的键值的value
        """
        node = self._getNode(key)    # 先判断字典中是否存在这个键
        if node != None:             # 已经存在
            node.value = value       # 更新这个Node的value
        else:
            self._dummyhead.next = Node(key, value, self._dummyhead.next)  # 否则在头部添加,添加操作链表那一章有讲,这里不再赘述
            self._size += 1          # 维护self._size

    def set(self, key, new_value):
        """
        Description: 将字典中键为key的Node的value设为new_value。注意,为防止与add函数发生混淆,
        此函数默认用户已经确信key在字典中,否则报错。并不会有什么新建Node的操作,因为这么做为与add函数有相同的功能,就没有意义了。
        时间复杂度:O(n)
        Params:
        - key: 将要被设定的Node的键
        - new_value: 新的value值
        """
        node = self._getNode(key)    # 找到携带这个key的Node
        if node is None:             # 没找到
            raise Exception('%s doesn\'t exist!' % key)   #报错就完事了
        node.value = new_value       # 找到了就直接将返回节点的value设为new_value

    def remove(self, key):
        """
        Description: 将字典中键为key的Node删除。注:若不存在携带key的Node,返回Node就好。
        时间复杂度:O(n)
        Params:
        - key: 待删除的键
        Returns:
        被删除节点的value
        """
        pre = self._dummyhead         # 找到要被删除节点的前一个节点(惯用手法,不再赘述)
        while pre.next is not None:   # pre的next只要不为空
            if pre.next.key == key:   # 如果找到了
                break                 # 直接break,此时pre停留在要被删除节点的前一个节点
            pre = pre.next            # 否则往后撸
        
        if pre.next is not None:      # 此时找到了
            delNode = pre.next        # 记录一下要被删除的节点,方便返回其value
            pre.next = delNode.next   # 不再赘述,如果不懂就去看看链表那节吧。O(∩_∩)O
            delNode.next = None       # delNode的下一个节点设为None,使delNode完全与字典脱离,便于垃圾回收器回收
            self._size -= 1           # 维护self._size
            return delNode.value      # 返回被删除节点的value
        
        return None                   # 此时pre的next是None!说明并没有找到这个key,返回None就好了。

    def printLinkedListDict(self):
        """打印字典元素"""
        cur = self._dummyhead.next 
        while cur != None:
            print('[Key: %s, Value: %s]' % (cur.key, cur.value), end='-->')
            cur = cur.next
        print('None')
  
    # private functions
    def _getNode(self, key):
        """
        Description: 一个辅助函数,是私有函数。功能就是返回要查找的键的Node,若key不存在就返回None
        时间复杂度:O(n)
        Params:
        - key: 要查找的键值
        Returns:
        返回带查找key的节点,若不存在返回None
        """
        cur = self._dummyhead.next   # 记录当前节点
        while cur != None:           # cur没到头       
            if cur.key == key:       # 找到了
                return cur           # 返回当前的Node
            cur = cur.next           # 没找到就往后撸
        return None                  # cur此时为None了。。说明已经到头还没找到,返回None

2. 测试

from DICT.linkedlistdict import LinkedListDict  # 我们的字典类写在了linkedlistmap.py文件中

test_map = LinkedListDict()
print('初始字典Size:', test_map.getSize())
print('初始字典是否为空:', test_map.isEmpty())
for i in range(10):
    test_map.add(i, str(i) + '_index')
print('10次添加操作后:')
print('Size: ', test_map.getSize())
print('是否包含7?', test_map.contains(7))
test_map.printLinkedListDict()  # 由于在头部插入,所以打印是反向的哦~
print('键为6的value:', test_map.get(6))
print('将键值为4的value设为 "你好呀",并打印:')
test_map.set(4, '你好呀')
test_map.printLinkedListDict()
# test_map.set(12, 'debug')
print('删除键为7的元素,并打印:')
test_map.remove(7)
test_map.printLinkedListDict()
print('此时的Size为:', test_map.getSize())

3. 输出

初始字典Size: 0
初始字典是否为空: True
10次添加操作后:
Size:  10
是否包含7True
[Key: 9, Value: 9_index]-->[Key: 8, Value: 8_index]-->[Key: 7, Value: 7_index]-->[Key: 6, Value: 6_index]-->[Key: 5, Value: 5_index]-->[Key: 4, Value: 4_index]-->[Key: 3, Value: 3_index]-->[Key: 2, Value: 2_index]-->[Key: 1, Value: 1_index]-->[Key: 0, Value: 0_index]-->None
键为6的value: 6_index
将键值为4的value设为 "你好呀",并打印:
[Key: 9, Value: 9_index]-->[Key: 8, Value: 8_index]-->[Key: 7, Value: 7_index]-->[Key: 6, Value: 6_index]-->[Key: 5, Value: 5_index]-->[Key: 4, Value: 你好呀]-->[Key: 3, Value: 3_index]-->[Key: 2, Value: 2_index]-->[Key: 1, Value: 1_index]-->[Key: 0, Value: 0_index]-->None
删除键为7的元素,并打印:
[Key: 9, Value: 9_index]-->[Key: 8, Value: 8_index]-->[Key: 6, Value: 6_index]-->[Key: 5, Value: 5_index]-->[Key: 4, Value: 你好呀]-->[Key: 3, Value: 3_index]-->[Key: 2, Value: 2_index]-->[Key: 1, Value: 1_index]-->[Key: 0, Value: 0_index]-->None
此时的Size为: 9

四、总结

  1. 本次略去了性能对比环节,因为两种底层和上一章的集合是一样的,所以性能对比结果参考集合那一章即可,肯定是基于二分搜索树的字典要比基于链表的块很多呢~
  2. 写到这里,可以看到二分搜索树是非常有用非常有用的一个数据结构!一定要熟练掌握原理以及实现!

若有还可以改进、优化的地方,还请小伙伴们批评指正!

猜你喜欢

转载自blog.csdn.net/Annihilation7/article/details/83211392