人工智能导论学习笔记03——二叉查找树

人工智能导论学习笔记03——二叉查找树


肝!


二叉搜索树

二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。

性质如下:

  1. 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 任意节点的左、右子树也分别为二叉查找树。
  4. 没有键值相等的节点。

二叉排序树的查找过程和次优二叉树类似,通常采取二叉链表作为二叉排序树的存储结构。中序遍历二叉排序树可得到一个关键字的有序序列,一个无序序列可以通过构造一棵二叉排序树变成一个有序序列,构造树的过程即为对无序序列进行排序的过程。每次插入的新的结点都是二叉排序树上新的叶子结点,在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。搜索,插入,删除的复杂度等于树高,O(log(n)).

算法实现

1. 二叉排序树的查找算法

2. 在二叉排序树插入结点的算法

3. 在二叉排序树删除结点的算法

4. 二叉排序树性能分析

当二叉搜索树出现如下图情形时,效率最低:

img

代码实现

python
构造一个BST
class BST(object):
    def __init__(self, root=None):
        self.root = root

    @classmethod
    def build_from(cls, node_list):
        cls.size = 0
        key_to_node_dict = {}
        for node_dict in node_list:
            key = node_dict['key']
            key_to_node_dict[key] = BSTNode(key, value=key)   # 这里值暂时用 和 key一样的

        for node_dict in node_list:
            key = node_dict['key']
            node = key_to_node_dict[key]
            if node_dict['is_root']:
                root = node
            node.left = key_to_node_dict.get(node_dict['left'])
            node.right = key_to_node_dict.get(node_dict['right'])
            cls.size += 1
        return cls(root)


NODE_LIST = [
    {'key': 60, 'left': 12, 'right': 90, 'is_root': True},
    {'key': 12, 'left': 4, 'right': 41, 'is_root': False},
    {'key': 4, 'left': 1, 'right': None, 'is_root': False},
    {'key': 1, 'left': None, 'right': None, 'is_root': False},
    {'key': 41, 'left': 29, 'right': None, 'is_root': False},
    {'key': 29, 'left': 23, 'right': 37, 'is_root': False},
    {'key': 23, 'left': None, 'right': None, 'is_root': False},
    {'key': 37, 'left': None, 'right': None, 'is_root': False},
    {'key': 90, 'left': 71, 'right': 100, 'is_root': False},
    {'key': 71, 'left': None, 'right': 84, 'is_root': False},
    {'key': 100, 'left': None, 'right': None, 'is_root': False},
    {'key': 84, 'left': None, 'right': None, 'is_root': False},
]
bst = BST.build_from(NODE_LIST)

BST操作——查找

如何查找一个指定的节点呢,根据定义我们知道每个内部节点左子树的 key 都比它小,右子树的 key 都比它大,所以 对于带查找的节点 search_key,从根节点开始,如果 search_key 大于当前 key,就去右子树查找,否则去左子树查找。 一直到当前节点是 None 了说明没找到对应 key。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1v7f71jQ-1630486310564)(C:\Users\Vermouth\AppData\Roaming\Typora\typora-user-images\image-20210901143632179.png)]

好,撸代码:

    def _bst_search(self, subtree, key):
        if subtree is None:   # 没找到
            return None
        elif key < subtree.key:
            return self._bst_search(subtree.left, key)
        elif key > subtree.key:
            return self._bst_search(subtree.right, key)
        else:
            return subtree

    def get(self, key, default=None):
        node = self._bst_search(self.root, key)
        if node is None:
            return default
        else:
            return node.value

BST操作——获取最大和最小key的节点

其实还按照其定义,最小值就一直向着左子树找,最大值一直向右子树找,递归查找就行

    def _bst_min_node(self, subtree):
        if subtree is None:
            return None
        elif subtree.left is None:   # 找到左子树的头
            return subtree
        else:
            return self._bst_min_node(subtree.left)

    def bst_min(self):
        node = self._bst_min_node(self.root)
        return node.value if node else None

BST操作——插入

插入节点的时候我们需要一直保持 BST 的性质,每次插入一个节点,我们都通过递归比较把它放到正确的位置。 你会发现新节点总是被作为叶子结点插入。(请你思考这是为什么)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QzIOXTvS-1630486310568)(C:\Users\Vermouth\AppData\Roaming\Typora\typora-user-images\image-20210901144956021.png)]

    def _bst_insert(self, subtree, key, value):
        """ 插入并且返回根节点

        :param subtree:
        :param key:
        :param value:
        """
        if subtree is None:   # 插入的节点一定是根节点,包括 root 为空的情况
            subtree = BSTNode(key, value)
        elif key < subtree.key:
            subtree.left = self._bst_insert(subtree.left, key, value)
        elif key > subtree.key:
            subtree.right = self._bst_insert(subtree.right, key, value)
        return subtree

    def add(self, key, value):
        node = self._bst_search(self.root, key)
        if node is not None:   # 更新已经存在的 key
            node.value = value
            return False
        else:
            self.root = self._bst_insert(self.root, key, value)
            self.size += 1
            return True

BST操作——删除节点

删除操作相比上边的操作要麻烦很多,首先需要定位一个节点,删除节点后,我们需要始终保持 BST 的性质。 删除一个节点涉及到三种情况:

  • 节点是叶节点
  • 节点有一个孩子
  • 节点有两个孩子

我们分别来看看三种情况下如何删除一个节点:

删除叶节点

这是最简单的一种情况,只需要把它的父亲指向它的指针设置为 None 就好。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f01WwcUF-1630486310571)(C:\Users\Vermouth\AppData\Roaming\Typora\typora-user-images\image-20210901145934711.png)]

删除只有一个孩子的节点

删除有一个孩子的节点时,我们拿掉需要删除的节点,之后把它的父亲指向它的孩子就行,因为根据 BST 左子树都小于节点,右子树都大于节点的特性,删除它之后这个条件依旧满足。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmuWNnyN-1630486310573)(C:\Users\Vermouth\AppData\Roaming\Typora\typora-user-images\image-20210901150016218.png)]

删除有两个孩子的内部节点

假如我们想删除 12 这个节点改怎么做呢?你的第一反应可能是按照下图的方式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JN4w2qWx-1630486310574)(C:\Users\Vermouth\AppData\Roaming\Typora\typora-user-images\image-20210901150049216.png)]

但是这种方式可能会影响树的高度,降低查找的效率。这里我们用另一种非常巧妙的方式。 还记得上边提到的吗,如果你中序遍历 BST 并且输出每个节点的 key,你会发现就是一个有序的数组。 [1 4 12 23 29 37 41 60 71 84 90 100]。这里我们定义两个概念,逻辑前任(predecessor)和后继(successor),请看下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jXDgcAiK-1630486310575)(C:\Users\Vermouth\AppData\Roaming\Typora\typora-user-images\image-20210901150111136.png)]

12 在中序遍历中的逻辑前任和后继分别是 4 和 23 节点。于是我们还有一种方法来删除 12 这个节点:

  • 找到待删除节点 N(12) 的后继节点 S(23)
  • 复制节点 S 到节点 N
  • 从 N 的右子树中删除节点 S,并更新其删除后继节点后的右子树

说白了就是找到后继并且替换,这里之所以能保证这种方法是正确的,你会发现替换后依旧是保持了 BST 的性质。 有个问题是如何找到后继节点呢?待删除节点的右子树的最小的节点不就是后继嘛,上边我们已经实现了找到最小 key 的方法了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gAbTCgE4-1630486310576)(C:\Users\Vermouth\AppData\Roaming\Typora\typora-user-images\image-20210901150133772.png)]

我们开始编写代码实现,和之前的操作类似,我们还是通过辅助函数的形式来实现,这个递归函数会比较复杂,请你仔细理解:

    def _bst_remove(self, subtree, key):
        """删除节点并返回根节点"""
        if subtree is None:
            return None
        elif key < subtree.key:
            subtree.left = self._bst_remove(subtree.left, key)
            return subtree
        elif key > subtree.key:
            subtree.right = self._bst_remove(subtree.right, key)
            return subtree
        else:  # 找到了需要删除的节点
            if subtree.left is None and subtree.right is None:    # 叶节点,返回 None 把其父亲指向它的指针置为 None
                return None
            elif subtree.left is None or subtree.right is None:  # 只有一个孩子
                if subtree.left is not None:
                    return subtree.left   # 返回它的孩子并让它的父亲指过去
                else:
                    return subtree.right
            else:  # 俩孩子,寻找后继节点替换,并从待删节点的右子树中删除后继节点
                successor_node = self._bst_min_node(subtree.right)
                subtree.key, subtree.value = successor_node.key, successor_node.value
                subtree.right = self._bst_remove(subtree.right, successor_node.key)
                return subtree

    def remove(self, key):
        assert key in self
        self.size -= 1
        return self._bst_remove(self.root, key)

完整代码你可以在本章的 bst.py 找到。 另外推荐一个可以在线演示过程的网址大家可以手动执行下看看效果: https://www.cs.usfca.edu/~galles/visualization/BST.html

时间复杂度分析

上边介绍的操作时间复杂度和二叉树的形状有关。平均来说时间复杂度是和树的高度成正比的,树的高度 h 是 log(n), 但是最坏情况下以上操作的时间复杂度都是 O(n)。为了改善 BST 有很多变种,感兴趣请参考延伸阅读中的内容。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DLK126iH-1630486310577)(C:\Users\Vermouth\AppData\Roaming\Typora\typora-user-images\image-20210901150630137.png)]

练习题:

  • 请你实现查找 BST 最大值的函数

延伸阅读

  • 《Data Structures and Algorithms in Python》14 章,树的概念和算法还有很多,我们这里介绍最基本的帮你打个基础
  • 了解红黑树。普通二叉查找树有个很大的问题就是难以保证树的平衡,极端情况下某些节点可能会非常深,导致查找复杂度大幅退化。而平衡二叉树就是为了解决这个问题。请搜索对应资料了解下。
  • 了解 mysql 索引使用的 B-Tree 结构(多路平衡查找树),这个是后端面试数据库的常考点。想想为什么?当元素非常多的时候,二叉树的深度会很深,导致多次磁盘查找。从B树、B+树、B*树谈到R 树

Leetcode

验证是否是合法二叉搜索树 [validate-binary-search-tree](https://leetcode.com/problems/validate-binary-search-tree/

附录

C++代码实现:
#include<iostream>
using namespace std;
 
//二叉搜索树结构
template<class K, class V>
struct BSTreeNode
{
	BSTreeNode* _left;
	BSTreeNode* _right;
	K _key;
	V _value;
 
	BSTreeNode(const K& key, const V& value)
		:_left(NULL)
		,_right(NULL)
		,_key(key)
		,_value(value)
	{}
 
};
 
template<class K,class V>
class BSTree
{
	typedef BSTreeNode<K, V> Node;
public:
	BSTree()
		:_root(NULL)
	{}
	
	//在二叉搜索树中插入节点
	bool Insert(const K& key, const V& value)
	{
		if (_root == NULL)
		{
			_root = new Node(key, value);
		}
 
		Node* cur=_root;
		Node* parent = NULL;
		//首先找到要插入的位置
		while (cur)
		{
			if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if(cur->_key<key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
		//在找到插入位置以后,判断插入父亲节点的左边还是右边
		if (parent->_key > key)
		{
			parent->_left = new Node(key, value);
		}
		else
		{
			parent->_right = new Node(key, value);
		}
 
		return true;
	}
 
 
	//在二叉搜索树中查找节点
	Node* Find(const K& key)
	{
		Node* cur=_root;
		//开始遍历查找
		while (cur)
		{
			if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else if(cur->_key<key)
			{
				cur = cur->_right;
			}
			else
			{
				return cur;
			}
		}
		 
		return NULL;
	}
 
 
	//在二叉搜索树中删除节点
	bool Remove(const K& key)
	{
		//没有节点
		if (_root == NULL)
		{
			return false;
		}
		//只有一个节点
		if (_root->_left == NULL&&_root->_right == NULL)
		{
			if (_root->_key == key)
			{
				delete _root;
				_root = NULL;
				return true;
			}
 
			return false;
		}
 
		Node* parent = NULL;
		Node* cur = _root;
		//遍历查找要删除节点的位置
		while (cur)
		{
			Node* del = NULL;
			if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				//要删除节点的左子树为空,分3种情况
				if (cur->_left == NULL)
				{
					//注意判断父节点是否为空,若为空,则要删除的节点为根节点,如:只有根节点5和其右节点9
					if (parent == NULL)
					{
						_root = cur->_right;
						delete cur;
						cur = NULL;
						return true;
					}
					if (parent->_key > cur->_key)
					{
						del = cur;
						parent->_left = cur->_right;
						delete del;
						return true;
					}
					else if (parent->_key < key)
					{
						del = cur;
						parent->_right = cur->_right;
						delete del;
						return true;
					}
				}
				//要删除节点的右子树为空,同样分3种情况
				else if (cur->_right == NULL)
				{
					//注意判断父节点是否为空,若为空,则要删除的节点为根节点,如:只有根节点5和其左节点3
					if (parent == NULL)
					{
						_root = cur->_left;
						delete cur;
						cur = NULL;
						return true;
					}
					if (parent->_key > cur->_key)
					{
						del = cur;
						parent->_left = cur->_left;
						delete del;
						return true;
					}
					else if (parent->_key < cur->_key)
					{
						del = cur;
						parent->_right = cur->_left;
						delete del;
						return true;
					}
				}
				//左右子树都不为空
				else
				{
					Node* del = cur;
					Node* parent = NULL;
					Node* RightFirst = cur->_right;
					//右边第一个节点的左子树为空
					if (RightFirst->_left == NULL)
					{
						swap(RightFirst->_key, cur->_key);
						swap(RightFirst->_value, cur->_value);
						del = RightFirst;
						cur->_right = RightFirst->_right;
						delete del;
						return true;
					}
					//右边第一个节点的左子树不为空
					while (RightFirst->_left)
					{
						parent = RightFirst;
						RightFirst = RightFirst->_left;
					}
					   swap(RightFirst->_key, cur->_key);
					   swap(RightFirst->_value, cur->_value);
					   del = RightFirst;
					   parent->_left = RightFirst->_right;
					   delete del;
				       return true;
				}
			}
		}
		return false;
	}
 
	bool Insert_R(const K& key, const V& value)
	{
		return _Insert_R(_root, key, value);
	}
 
	Node* Find_R(const K& key)
	{
		return _Find_R(_root, key);
	}
 
	bool Remove_R(const K& key)
	{
		return _Remove_R(_root, key);
	}
 
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
 
protected:
		
		bool _Remove_R(Node*& root, const K& key)
		{
			//没有节点
			if (root == NULL)
			{
				return false;
			}
			//只有一个节点
			if (root->_left == NULL&&root->_right == NULL)
			{
				if (root->_key == key)
				{
					delete root;
					root = NULL;
					return true;
				}
				else
				{
					return false;
				}
 
			}
 
			//删除二叉搜索树节点的递归写法
			if (root->_key > key)
			{
				_Remove_R(root->_left, key);
			}
			else if (root->_key < key)
			{
				_Remove_R(root->_right, key);
			}
			else
			{
				Node* del = NULL;
				
				if (root->_left == NULL)
				{
					del = root;
					root = root->_right;
					delete del;
					del = NULL;
					return true;
				}
				else if (root->_right == NULL)
				{
					del = root;
					root = root->_left;
					delete del;
					del = NULL;
					return true;
				}
				else
				{
					Node* RightFirst = root->_right;
 
					while (RightFirst->_left)
					{
						RightFirst = RightFirst->_left;
					}
 
					swap(root->_key, RightFirst->_key);
					swap(root->_value, RightFirst->_value);
 
					_Remove_R(root->_right, key);
					return true;
				}
			}
		}
 
		//递归查找法
		Node* _Find_R(Node* root, const K& key)
		{
			if (root == NULL)
			{
				return NULL;
			}
			if (root->_key > key)
			{
				return _Find_R(root->_left, key);
			}
			else if (root->_key < key)
			{
				return _Find_R(root->_right, key);
			}
			else
			{
				return root;
			}
		}
			
		//递归插入法
		bool _Insert_R(Node*& root, const K& key, const V& value)
		{
			if (root == NULL)
			{
				root = new Node(key, value);
				return true;
			}
			if (root->_key > key)
			{
				return _Insert_R(root->_left, key, value);
			}
			else if(root->_key < key)
			{
				return _Insert_R(root->_right, key, value);
			}
			else
			{
				return false;
			}
		}
 
		void _InOrder(Node* root)
		{
			if (root == NULL)
			{
				return;
			}
 
			_InOrder(root->_left);
			cout << root->_key << " ";
			_InOrder(root->_right);
		}
protected:
	Node* _root;
 
};
 
 
void Test()
{
	BSTree<int, int> s;
	
	//测试插入
	s.Insert_R(5, 1);
	s.Insert_R(4, 1);
	s.Insert_R(3, 1);
	s.Insert_R(6, 1);
	s.Insert_R(1, 1);
	s.Insert_R(2, 1);
	s.Insert_R(0, 1);
	s.Insert_R(9, 1);
	s.Insert_R(8, 1);
	s.Insert_R(7, 1);
 
	//二叉搜索树按中序输出是有序的
	s.InOrder();
 
	//测试查找
	cout << s.Find_R(6)->_key << endl;
 
	//测试删除
	s.Remove(4);
	s.Remove(6);
	s.Remove(3);
	s.Remove(1);
	s.Remove(2);
	
	//再次打印删除后的结果
	s.InOrder();
 
}
 
int main()
{
	Test();
	system("pause");
	return 0;
}

Python代码实现:

class BSTNode(object):
    def __init__(self, key, value, left=None, right=None):
        self.key, self.value, self.left, self.right = key, value, left, right

class BST(object):
    def __init__(self, root=None):
        self.root = root

    @classmethod
    def build_from(cls, node_list):
        cls.size = 0
        key_to_node_dict = {}
        for node_dict in node_list:
            key = node_dict['key']
            key_to_node_dict[key] = BSTNode(key, value=key)  # 这里值暂时用 和 key

        for node_dict in node_list:
            key = node_dict['key']
            node = key_to_node_dict[key]
            if node_dict['is_root']:
                root = node
            node.left = key_to_node_dict.get(node_dict['left'])
            node.right = key_to_node_dict.get(node_dict['right'])
            cls.size += 1
        return cls(root)

    '''查找'''
    def _bst_search(self, subtree, key):
        if subtree is None:  # 没找到
            return None
        elif key < subtree.key:
            return self._bst_search(subtree.left, key)
        elif key > subtree.key:
            return self._bst_search(subtree.right, key)
        else:
            return subtree

    def get(self, key, default=None):
        node = self._bst_search(self.root, key)
        if node is None:
            return default
        else:
            return node.value

    '''获取最大和最小key节点'''
    def _bst_min_node(self, subtree):
        if subtree is None:
            return None
        elif subtree.left is None:  # 找到左子树的头
            return subtree
        else:
            return self._bst_search(subtree.left)

    '''插入'''
    def _bst_insert(self, subtree, key, value):
        """ 插入并返回根节点

        :param subtree:
        :param key:
        :param value:
        :return:
        """
        if subtree is None: # 插入的节点一定是跟节点,包括root为空的情况
            subtree = BSTNode(key, value)
        elif key < subtree.key:
            subtree.left = self._bst_insert(subtree.left, key, value)
        elif key > subtree.key:
            subtree.right = self._bst_insert(subtree.right, key)
        return subtree

    def add(self, key, value):
        node = self._bst_search(self.root, key)
        if node is not None:  # 更新已经存在的key
            node.value = value
            return False
        else:
            self.root = self._bst_insert(self.root, key, value)
            self.size += 1
            return True

    '''删除节点'''
    def _bst_remove(self, subtree, key):
        """删除节点并返回根节点"""
        if subtree is None:
            return None
        elif key < subtree.key:
            subtree.left = self._bst_remove(subtree.left, key)
            return subtree
        elif key > subtree.key:
            subtree.right = self._bst_remove(subtree.right, key)
            return subtree
        else: # 找到了需要删除的节点
            if subtree.left is None and subtree.right is None:   # 叶节点,返回None 把其父亲指向它的指针置为None
                return None
            elif subtree.left is None or subtree.right is None:    # 只有一个孩子
                if subtree.left is not None:
                    return subtree.left
                else:
                    return subtree.right
            else:    #俩孩子,寻找后继节点替换,并从待删节点的右子树中删除后继节点
                successor_node = self._bst_min_node(subtree.right)
                subtree.key, subtree.value = successor_node.key, successor_node.value
                subtree.right = self._bst_remove(subtree.right, successor_node.key)
                return subtree

    def remove(self,key):
        assert key in self
        self.size -= 1
        return self._bst_remove(self.root, key)

NODE_LIST = [
    {'key': 60, 'left': 12, 'right': 90, 'is_root': True},
    {'key': 12, 'left': 4, 'right': 41, 'is_root': False},
    {'key': 4, 'left': 1, 'right': None, 'is_root': False},
    {'key': 1, 'left': None, 'right': None, 'is_root': False},
    {'key': 41, 'left': 29, 'right': None, 'is_root': False},
    {'key': 29, 'left': 23, 'right': 37, 'is_root': False},
    {'key': 23, 'left': None, 'right': None, 'is_root': False},
    {'key': 37, 'left': None, 'right': None, 'is_root': False},
    {'key': 90, 'left': 71, 'right': 100, 'is_root': False},
    {'key': 71, 'left': None, 'right': 84, 'is_root': False},
    {'key': 100, 'left': None, 'right': None, 'is_root': False},
    {'key': 84, 'left': None, 'right': None, 'is_root': False},
]
bst = BST.build_from(NODE_LIST)

参考

二叉查找树

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值