算法学习--二叉树

本文探讨了二叉树及二叉查找树的各种经典算法,包括但不限于二叉查找树转化为排序双链表、二叉树非递归遍历、LCA问题解决方法等。文章深入浅出地讲解了算法原理及其实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

二分查找树转化为排序的循环双链表

输入一棵二元查找树,将该二元查找树转换成一个排序的双向链表。
要求不能创建任何新的结点,只调整指针的指向。

例如对于下面的二分查找树:

ordered binary tree

small pointer 其实也就是指向左孩子,large pointer指向右孩子,转化为双链表之后 small pointer应该指向前一个元素,large pointer应该指向后一个元素。转化为排序的循环双链表则为:

circular doubly linked list

中序遍历改变指针方向

void inOrderBSTree(BSTreeNode* pBSTree)
{
 
    if (NULL==pBSTree)
    {
        return;
    }
    if (NULL!=pBSTree->m_pLeft)
    {
        inOrderBSTree(pBSTree->m_pLeft);
    }
 
    //  if (NULL!=pBSTree)
    //  {
    //      cout<<pBSTree->m_nValue;
    //  }
    convertToDoubleList(pBSTree);
 
    if (NULL!=pBSTree->m_pRight)
    {
        inOrderBSTree(pBSTree->m_pRight);
    }
 
}
/************************************************************************/
 
/************************************************************************/
/* 调整结点指针                                                                   */
void convertToDoubleList(BSTreeNode* pCurrent)
{
    pCurrent->m_pLeft=pIndex;//使当前结点的左指针指向双向链表中最后一个结点
    if (NULL==pIndex)//若最后一个元素不存在,此时双向链表尚未建立,因此将当前结点设为双向链表头结点
    {
        pHead=pCurrent;
    }
    else//使双向链表中最后一个结点的右指针指向当前结点
    {
        pIndex->m_pRight=pCurrent;
    }
 
    pIndex=pCurrent;//将当前结点设为双向链表中最后一个结点
 
    cout<<pCurrent->m_nValue<<" ";
 
}

LCA:
解法1:下面是一个简单的复杂度为 O(n) 的算法,解决LCA问题
1) 找到从根到n1的路径,并存储在一个向量或数组中。
2)找到从根到n2的路径,并存储在一个向量或数组中。
3) 遍历这两条路径,直到遇到一个不同的节点,则前面的那个即为最低公共祖先.
bool findpath(Node * root,vector<int> &path,int key){
    if(root == NULL) return false;
    path.push_back(root->key);
    if(root->key == key) return true;
    //左子树或右子树 是否找到,找到的话当前节点就在路径中了
    bool find =  ( findpath(root->left, path, key) || findpath(root->right,path ,key) );
    if(find) return true;
    //该节点下未找到就弹出
    path.pop_back();
    return false;
}
 
int findLCA(Node * root,int key1,int key2){
    vector<int> path1,path2;
    bool find1 = findpath(root, path1, key1);
    bool find2 = findpath(root, path2, key2);
    if(find1 && find2){
        int ans ;
        for(int i=0; i<path1.size(); i++){
            if(path1[i] != path2[i]){
                break;
            }else
                ans = path1[i];
        }
        return ans;
    }
    return -1;
}

解法2:

上面的方法虽然是O(n),但是操作依然繁琐了一点,并且需要额外的空间来存储路径。其实可以只遍历一次,利用递归的巧妙之处。学好二叉树,其实就是学好递归。

从root开始遍历,如果n1和n2中的任一个和root匹配,那么root就是LCA。 如果都不匹配,则分别递归左、右子树,如果有一个 key(n1或n2)出现在左子树,并且另一个key(n1或n2)出现在右子树,则root就是LCA.  如果两个key都出现在左子树,则说明LCA在左子树中,否则在右子树。

struct Node *findLCA(struct Node* root, int n1, int n2)
{
    if (root == NULL) return NULL;
    // 只要n1 或 n2 的任一个匹配即可
    //  (注意:如果 一个节点是另一个祖先,则返回的是祖先节点。因为递归是要返回到祖先的 )
    if (root->key == n1 || root->key == n2)
        return root;
    // 分别在左右子树查找
    Node *left_lca  = findLCA(root->left, n1, n2);
    Node *right_lca = findLCA(root->right, n1, n2);
    // 如果都返回非空指针 Non-NULL, 则说明两个节点分别出现了在两个子树中,则当前节点肯定为LCA
    if (left_lca && right_lca)  return root;
    // 如果一个为空,在说明LCA在另一个子树
    return (left_lca != NULL)? left_lca: right_lca;
}

不使用递归和栈中序遍历二叉树

我们知道,在深度搜索遍历的过程中,之所以要用递归或者是用非递归的栈方式,参考二叉树非递归中序遍历,都是因为其他的方式没法记录当前节点的parent,而如果在每个节点的结构里面加个parent 分量显然是不现实的,那么Morris是怎么解决这一问题的呢?好吧,他用得很巧妙,实际上是用叶子节点的空指针来记录当前节点的位置,然后一旦遍历到了叶子节点,发现叶子节点的右指针指向的是当前节点,那么就认为以当前节点的左子树已经遍历完成。Morris 遍历正是利用了线索二叉树 的思想。

以inorder为例,初始化当前节点为root,它的遍历规则如下:

  • 如果当前节点为空,程序退出。
  • 如果当前节点非空,
    • 如果当前节点的左儿子为空,那么输出当前节点,当前节点重置为当前节点的右儿子。
    • 如果当前节点的左儿子非空,找到当前节点左子树的最右叶子节点(此时最右节点的右儿子有两种情况,一种是指向当前节点,一种是为空,你也许感到奇怪,右节点的右儿子怎么可能非空,注意,这里的最右叶子节点只带的是原树中的最右叶子节点。),若其最右叶子节点为空,令其指向当前节点,将当前节点重置为其左儿子,若其最右节点指向当前节点,输出当前节点,将当前节点重置为当前节点的右儿子,并恢复树结构,即将最右节点的右节点再次设置为NULL
void MorrisTraversal(struct tNode *root)
{
  struct tNode *current,*pre;
 
  if(root == NULL)
     return;
 
  current = root;
  while(current != NULL)
  {                
    if(current->left == NULL)
    {
      printf(" %d ", current->data);
      current = current->right;     
    }   
    else
    {
      /* 找到current的前驱节点 */
      pre = current->left;
      while(pre->right != NULL && pre->right != current)
        pre = pre->right;
 
      /* 将current节点作为其前驱节点的右孩子 */
      if(pre->right == NULL)
      {
        pre->right = current;
        current = current->left;
      }
 
      /* 恢复树的原有结构,更改right 指针 */  
      else
      {
        pre->right = NULL;
        printf(" %d ",current->data);
        current = current->right;     
      } /* End of if condition pre->right == NULL */
    } /* End of if condition current->left == NULL*/
  } /* End of while */
}

有序链表转化为平衡的二分查找树

比较直观的解法是自顶向下的递归解决,先找到中间节点作为根节点,然后递归左右两部分。所有我们需要先找到中间节点,对于单链表来说,必须要遍历一边,可以使用快慢指针加快查找速度。
public class Solution {
    public TreeNode sortedListToBST(ListNode head) {
        if(head == null) return null;
        if(head.next == null){
            return new TreeNode(head.val);
        }
        //用快慢指针找到中间节点
        ListNode slow = head;
        ListNode fast = head;
        ListNode preSlow = null;
        while(fast.next != null && fast.next.next != null){
            preSlow = slow;
            slow = slow.next;
            fast = fast.next.next;
        }
        //分别递归左右两部分
        TreeNode mid = new TreeNode(slow.val);
        if(preSlow != null){
            preSlow.next = null;
            mid.left = sortedListToBST(head);
        }
        if(slow.next != null){
            mid.right = sortedListToBST(slow.next);
        }
        return mid;
    }
}

找出二叉树中某个节点的所有祖先节点

对于一颗普通的二叉树和一个节点key,找出该节点的所有祖先节点。

例如树结构如下:给定的key为节点7,则应该打印 4,2,1.

bool printAncestors(struct node *root, int target)
{
  if (root == NULL)
     return false;
 
  if (root->data == target)
     return true;
  //子树可以找到,当前节点肯定为祖先节点
  if ( printAncestors(root->left, target) ||
       printAncestors(root->right, target) )
  {
    cout << root->data << " ";
    return true;
  }
  /* Else return false */
  return false;
}

二叉树非递归中序遍历

void InOrderTraverse(BiTree T)//非递归中序遍历  
{  
      
    stack<BiTree> Stack;  
    if(!T)  
    {  
        printf("空树!\n");  
        return;  
    }  
      
    while(T || !Stack.empty())  
    {  
        while(T)  
        {  
            Stack.push(T);  
            T=T->lchild;  
        }  
        T=Stack.top();  
        Stack.pop();  
        printf("%c",T->data);  
        T=T->rchild;  
    }                                                                                                                                     
}  

二叉查找树的后序遍历结果

题目:输入一个整数数组,判断该数组是不是某二元查找树的后序遍历的结果。如果是返回true,否则返回false。

例如输入5、7、6、9、11、10、8,由于这一整数序列是如下树的后序遍历结果:

8
/  \
6    10
/ \    / \
5   7   9  11

因此返回true。

如果输入7、4、6、5,没有哪棵树的后序遍历的结果是这个序列,因此返回false。

分析:这是一道trilogy的笔试题,主要考查对二元查找树的理解。

在后续遍历得到的序列中,最后一个元素为树的根结点。从头开始扫描这个序列,比根结点小的元素都应该位于序列的左半部分;从第一个大于跟结点开始到跟结点前面的一个元素为止,所有元素都应该大于跟结点,因为这部分元素对应的是树的右子树。根据这样的划分,把序列划分为左右两部分,我们递归地确认序列的左、右两部分是不是都是二元查找树。

bool verifySquenceOfBST(int squence[], int length)
{
      if(squence == NULL || length <= 0)
            return false;
 
      // root of a BST is at the end of post order traversal squence
      int root = squence[length - 1];
 
      // the nodes in left sub-tree are less than the root
      int i = 0;
      for(; i < length - 1; ++ i)
      {
            if(squence[i] > root)
                  break;
      }
 
      // the nodes in the right sub-tree are greater than the root
      int j = i;
      for(; j < length - 1; ++ j)
      {
            if(squence[j] < root)
                  return false;
      }
 
      // verify whether the left sub-tree is a BST
      bool left = true;
      if(i > 0)
            left = verifySquenceOfBST(squence, i);
 
      // verify whether the right sub-tree is a BST
      bool right = true;
      if(i < length - 1)
            right = verifySquenceOfBST(squence + i, length - i - 1);
      return (left && right);
}






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值