恢复有两个节点被调换的二进制搜索树(算法指南)

402 阅读5分钟

在这篇文章中,我们将开发并实现一种算法,以恢复被意外调换了两个节点的二进制搜索树(BST)。

目录:

  1. 审视问题陈述
  2. 解决问题
  3. 在一个排序的数组中找到两个被调换的元素
  4. 结论

要练习更多的二叉树问题,请浏览这个二叉树问题列表
这与Leetcode问题99相似。 恢复二叉搜索树。让我们开始练习恢复有两个节点被调换的二进制搜索树。

检查问题陈述

像往常一样,在我们开始实施一个问题的解决方案之前,我们应该确保我们对问题有一个深刻的理解。让我们从定义一个明确的问题陈述开始:

给出一个二进制搜索树的根,其中正好有两个节点被意外地调换了。在不改变树的结构的情况下恢复BST。

好了,我们现在有了一个明确的问题陈述,我们知道我们要恢复一棵BST,但BST到底是什么?

二进制搜索树是一种二进制树形数据结构,每个节点都包含一个键,它保持着一些特殊的属性:

  1. 一个节点的左边子树上的所有节点的键都小于该节点的键
  2. 一个节点的右子树中的所有节点的键都大于该节点的键。
  3. 左边和右边的子树也是二进制搜索树

bst
BST的例子

二进制搜索树是很酷的数据结构,对插入、查找和删除都很有用。所有提到的操作的时间复杂度为O(h),其中h在树被平衡的平均和最佳情况下等于log(n),或者在树不平衡的最坏情况下h等于n。

解决问题

为了恢复BST,我们必须首先找到违规的节点,然后交换它们的值,以便保留BST属性。让我们来看看我们可以采用哪些策略来找到被交换的节点。

为了找到违规的节点,我们必须能够在树上的每个节点中移动,并读取它们的键。在树上移动被称为树的遍历。有两种方法来遍历一棵树,即深度优先或广度优先。

广度优先的遍历需要逐级遍历树。你从根部开始,然后访问根部的直系子孙,再访问直系子孙的直系子孙,如此循环。而在深度优先中,你总是通过层层推进,直到你到达一个叶子节点,然后回溯。有3种深度优先的遍历方式:

  1. 无序遍历
  2. 前序遍历
  3. 后序遍历

对于这个问题,我们只对Inorder遍历感兴趣。在无序遍历中,我们递归地探索左子树,然后是根,然后是右子树。

为什么我们对树的无序遍历感兴趣?因为一个正确的BST的无序遍历将给出一个按键值递增排序的节点列表。这是一个值得注意的属性,它将帮助我们解决问题。

Python中树的无序遍历的实现:

class BST:
    """ representation of a binary search tree"""
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

def inordertraversal(rootnode):
    """ store the inorder traversal of a bst in a list and return it """
    nodesInorder = []
    
    def inorder(node):
        """ recursive function that stores the inorder traversal into a list """
        if not node:
            return
        else:
            # inorder traversal left-node-right
            inorder(node.left)
            nodesinorder.append(node)
            inorder(node.right)
    
    # call the recursive function and populate the list
    inorder(rootnode)
    
    return nodesinorder

然后我们继续得到我们受损的BST的无序遍历列表。创建一个列表的副本,对副本进行排序,并与原始列表进行比较,找到两个违规的节点,然后将其交换,**从而恢复树。**这种方法的时间复杂度为O(N logN)。

另外,由于我们知道列表中只有两个条目的位置是错误的。这个问题可以简化为在一个排序的数组中找到两个被交换的元素的问题。这可以通过一次解决,时间复杂度为O(n)

在一个排序的数组中找到两个互换的元素

我们有一个几乎被排序的列表**[3, 10, 5, 6, 4, 14]。我们可以直观地看到,10和4的值被交换了。这个交换打断了列表中数字的流动,这些数字应该是按递增的顺序排列的,但是在10、56、4**这两个点上,这个属性被违反了。

为了找到有问题的节点,我们在列表中迭代检查,当位置i的值大于位置i+1的值时,表明违反了递增特性。第一次遇到这种情况时,我们将位置i(较大的数字)存储在一个单独的故障列表中,第二次遇到这种情况时,我们将位置i+1存储起来。然后我们交换位置上的值。

上述算法的一个边缘情况是当两个被交换的值彼此相邻时。在这种情况下,循环将永远找不到第二次违反列表增加属性的情况。我们可以通过在迭代结束时增加一个检查来轻松缓解这种情况,如果有问题的列表中只有一个数字,这意味着下一个数字是相邻的,所以将其添加到列表中并进行交换。

实现方法

def restoreOrder(listofnodes):
    faultylist = []

    # add the index of the bigger of the swapped nodes
    for i in range(0, len(listofnodes) - 1):
        if listofnodes[i].val > listofnodes[i + 1].val:
            faultylist.append(i)
            break
            
    # add the index of the smaller of the swapped nodes
    for j in range(faultylist[0] + 1, len(listofnodes) - 1):
        if listofnodes[j].val > listofnodes[j + 1].val:
            faultylist.append(j + 1)
            break
    
    # check for edgecase where nodes are adjacent
    if len(faultylist) == 1:
        faultylist.append(faultylist[0] + 1)
    
    # swap the value of the nodes
    node1 = listofnodes[faultylist[0]]
    node2 = listofnodes[faultylist[1]]
    
    node1.val, node2.val = node2.val, node1.val

总结

我们已经能够解决BST中两个错误的交换节点的问题,方法是利用树的一个固有属性,即树的无序遍历会导致一个排序的列表。然后,我们利用这一特性将问题简化为一个更简单的问题,即在一个排序的列表中找到两个被交换的节点。这使我们能够在线性时间内完全解决这个问题。