面试基础算法及编程 第三弹(树(二叉树)相关:主要考察指针相关的操作)

// #  -*- coding:utf-8 -*- 
// #  @Author: Mr.chen(ai-chen2050@qq.com) 
// #  @Date: 2018-08-17 16:32:55 

// 注:此为第三弹,主要讲解树(但是面试中大多提到的都是二叉树)相关面试笔试题,要求手写。
// 在树的面试题中也会涉及到大量的指针,对于二叉树来说最应该先关注的就是它的前中后序遍历,
// 对于它们来说都有递归和非递归遍历,还有层次遍历,说明如下图所示:

 

     

       


/* 
1、重建二叉树,输入某二叉树的前序和中序,请重建该二叉树。假设输入的前序和中序序列都不含重复
   的数字,例如输入前序的序列为{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6} 
   思路:首先前序序列中的第一个节点一定是根节点,中序序列中,根节点在中间,其左右子树在其两边,
   故我们可以根据中序找到根节点,左边的即为左子树,右边的即为右左子树,之后再其左右两边的序列
   递归去构造生成二叉树。代码如下:
*/

#include<iostream>
#include<set>
#include<map>
#include<stack>
#include<deque>
#include<list>
#include<stdexcept>

using namespace std;

// 二叉树的节点结构体为:如下。
struct BiTreeNode 
{
    int         m_value;
    BiTreeNode* m_pLeft;
    BiTreeNode* m_pRight;
};

BiTreeNode* Construct(int* preOrder,int* inOrder,int length)
{
    if(preOrder == NULL || inOrder == NULL || length <= 0)
        return NULL;
    return ConstructCore(preOrder,preOrder+length-1,inOrder,inOrder+length-1);
}

BiTreeNode* ConstructCore(int* sPreOrder,int* ePreOrder,int* sInOrder,int* eInOrder)
{
    // 前序遍历的第一个节点是根节点的值
    int rootValue = sPreOrder[0];
    BiTreeNode* root = new BiTreeNode();
    root->m_value = rootValue;
    root->m_pLeft = root->m_pRight = NULL;

    // 定义递归出口
    if(sPreOrder == ePreOrder)
    {
        if(sInOrder == eInOrder && *sPreOrder == *sInOrder)
            return root;
        else 
            throw std::exception("Invalid input.");
    }

    // 在中序中找到根节点的值
    int* rootInOrder = sInOrder;
    while(rootInOrder < eInOrder && *rootInOrder != rootValue)
        ++ rootInOrder;
    if(rootInOrder == eInOrder && *rootInOrder != rootValue)
        throw std::exception("Invalid input...")
    
    // 左子树的节点个数
    int leftLength = rootInOrder - sInOrder;
    int* leftPreOrderEnd = sPreOrder + leftLength;
    if(leftLength > 0)                                  //构建左子树   
        root->m_pLeft = ConstructCore(sPreOrder + 1,leftPreOrderEnd,sInOrder,rootInOrder - 1);
    if(leftLength < ePreOrder - sPreOrder)               //构建右子树
        root->m_pRight = ConstructCore(leftPreOrderEnd+1,ePreOrder,rootInOrder+1,eInOrder)

    return root;
}


/* 
2、求二叉树的深度:输入一颗二叉树的根节点,求该树的深度。从根节点到页节点依次经过的节点(含根和叶节点)
   形成树的一颗路径,最长路径的长度即为树的深度。思路:我们可以从另外一个角度来考虑树的深度。如果树只有
   一个节点,则深度为 1,如果根节点只有左子树而没有右子树,则树的深度应该是左子树加 1,反之,是右子树加 1
   ,故我们很容易想到递归实现。
*/
int treeDepth(BiTreeNode* pRoot)
{
    if(NULL == pRoot)
        return 0;
    // 计算左右子树的深度
    int nLeft = treeDepth(pRoot->m_pLeft);
    int nRight = treeDepth(pRoot->m_pRight);

    return nLeft > nRight? (nLeft + 1):(nRight + 1);
}


/* 
2.1 面试官加深难度, 追问: 输入一颗二叉树的根节点,判断它是不是平衡二叉树,
    如果某二叉树中任意节点的左右子树深度相差不超过 1,那么它就是一颗平衡二
    叉树。法一:需要重复遍历一个节点多次,基于上述代码的改进,法二:采用后
    序遍历的方法遍历每一个节点,在遍历一个节点之前,我们就已经遍历了该节点
    的左右子树。只要在遍历一个节点的时候,记录它的深度,就可以一边遍历,一
    边判断该节点是否平衡的了。
*/

//法一:需要重复遍历一个节点多次
bool isBalanced(BiTreeNode* pRoot)
{
    if(NULL == pRoot)
        return true;
    
    int left = treeDepth(pRoot->m_pLeft);
    int right = treeDepth(pRoot->m_pRight);
    int diff = left - right;
    if(diff > 1 || diff < -1)
        return false;
    
    // 递归判断,左右子树
    return isBalanced(pRoot->m_pLeft) && isBalanced(pRoot->m_pRight);
}   // 由于重复遍历会影响性能,故我们推荐下面这种基于后序遍历的方法。


// 法二:每个节点只遍历一次
bool isBalanced(BiTreeNode* pRoot,int* pDepth)
{
    if(NULL == pRoot)
    {
        *pDepth = 0;
        return true;
    }
    
    int left,right;
    if(isBalanced(pRoot->m_pLeft, &left) && isBalanced(pRoot->m_pRight, &right))
    {
        int diff = left - right;
        if(diff <= 1 && diff >= -1)
        {
            *pDepth = 1 + (left > right? left:right );
            return true;
        }
    }
    return false;
}

// 调用时,只需给上面的函数传入一个二叉树的根节点和表示节点深度的整形变量即可。
bool isBalancedTree(BiTreeNode* pRoot)
{
    int depth = 0;
    return isBalanced(pRoot, &depth);
}

/* 
3、树的子结构,题目:输入两颗二叉树 A 和 B,判断 B 是不是 A 的子结构,子结构定义如下图:
   一般来说,树的指针的操作比起链表来说更多,如果面试官想加大难度,则会考察树,在涉及
   二叉树的题目中,由于涉及到很多的指针操作,故一定要注意检查边界条件,即检查空指针,
   如果没有检查,而导致程序崩溃,则是很尴尬和忌讳的事情。故在操作指针时,一定要注意
   这个指针是不是为 NULL,如果是,该怎么处理。本题可以分两步来看,第一步是从树 A 中
   查找与根节点值一样的节点,实际上是对树的遍历,一般遍历树可以有递归和迭代循环,一般
   递归代码较为简单,一般会优先考虑,第二步是判断 A 树中以 R 为根节点的子树是不是和
   树 B 具有相同的结构,同样,我们也可以采用递归的方法来做。
*/

            


// 第一步: 递归遍历,寻找相同节点
bool hasSubTree(BiTreeNode* pRoot1,BiTreeNode* pRoot2)
{
    bool result = false;

    // 异常值检测
    if(NULL != pRoot1 && NULL != pRoot2)
    {
        if(pRoot1->m_value == pRoot2->m_value)
            result = doseTree1HasTree2(pRoot1,pRoot2);
        if(!result)
            result = hasSubTree(pRoot1->m_pLeft,pRoot2);
        if(!result)
            result = hasSubTree(pRoot1->m_pRight,pRoot2);
    }
    return result;
}

// 第二步:比较子结构,同样可以用递归的方法
bool doseTree1HasTree2(BiTreeNode* pRoot1,BiTreeNode* pRoot2)
{
    // 定义递归出口
    if(NULL == pRoot2)
        return true;
    if(NULL == pRoot1)
        return false;
    if(pRoot1->m_value != pRoot2->m_value)
        return false;
    return doseTree1HasTree2(pRoot1->m_pLeft,pRoot2->m_pLeft) &&
             doseTree1HasTree2(pRoot1->m_pRight,pRoot2->m_pRight);
}
/* 
4、二叉树的镜像(画图(几何的视角去看待)让抽象问题形象化),二叉树的镜像对大多数没有接触过得人来说,
   算是一个新的感念,此时可以通过几何画图的方式来让问题明朗化。如下图:
   通过观察图,我们来总结解决该问题的方法。首先交换根节点的两个子节点,此时子节点的子节点的左右
   子树的相对关系保持不变。故还需要交换子节点的子节点的左右子树,知道叶子节点。思路如下图,所示。
   通过前序遍历这棵树的每个节点,如果该节点有子节点,就交换该节点的两个子节点,当交换完所有的非
   叶子节点的左右子节点后,就得到了树的镜像。
*/

                                        

                       解题思路图,如下: 

               

void mirrorRecursively(BiTreeNode* pRoot)
{
    // 定义递归出口
    if(pRoot == NULL || (pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL))
        return;
    
    // 首先交换根节点
    BiTreeNode* pTemp = pRoot->m_pLeft;
    pRoot->m_pLeft = pRoot->m_pRight;
    pRoot->m_pRight = pTemp;

    // recursively
    if(pRoot->m_pLeft)
        mirrorRecursively(pRoot->m_pLeft);
    if(pRoot->m_pRight)
        mirrorRecursively(pRoot->m_pRight)
}

// 当使用循环时,需要一个栈来保存遍历到的当前的根结点
void mirrorCircularly(BiTreeNode* pRoot)
{
    // 用栈保存根结点
    std::stack<BiTreeNode* > stackRoot;

    if(pRoot == NULL)
        return;
    stackRoot.push(pRoot);
    // 开始循环
    while(!stackRoot.empty())
    {
        //从栈中获取根结点
        BiTreeNode* pNode = stackRoot.top();
        stackRoot.pop();

        // 首先交换根节点
        BiTreeNode* pTemp = pNode->m_pLeft;
        pNode->m_pLeft = pNode->m_pRight;
        pNode->m_pRight = pTemp;

        if(pNode->m_pLeft)
            stackRoot.push(pNode->m_pLeft);
        if(pNode->m_pRight)
            stackRoot.push(pNode->m_pRight);
    }   
}


/*  
5、从上到下打印二叉树,从上到下打印二叉树的每一个节点,同一层的节点按照从左到右的顺序打印。即层次遍历。
   其实,树是一种简化的图,该题也对应图的广度优先遍历。我们来尝试分析一下,由于是层次遍历,故最先打印
   头结点,然后我们需要一个容器来保存头结点的两个子节点,并且是先左后右,且容器应该保证先进先出(队列)
   ,之后只要队列里面的节点有孩子节点,我们就应该将孩子节点加载到队列末尾,以便后续遍历。直到叶子节点。
*/

// 由于 STL 已经帮我们实现了一个很好的双端队列 deque,故我们这里直接采用 deque 作为数据结构
void printFromTopToButtom(BiTreeNode* pRoot)
{
    if(NULL === pRoot)
        return;
    std::deque<BiTreeNode* > treeDeque;
    
    // 压入根节点
    treeDepth.push_back(pRoot);
    
    // 循环迭代
    while(treeDeque.size())
    {
        BiTreeNode* pNode = treeDeque.front();
        treeDeque.pop_front();

        std::cout<< pNode->m_value <<std::endl;

        // 将 pNode 的子节点加入队列
        if(pNode->m_pLeft)
            treeDeque.push_back(pNode->m_pLeft);
        if(pNode->m_pRight)
            treeDeque.push_back(pNode->m_pRight);
    }
}       // 本题的扩展图的广度优先遍历


/* 
6、二叉搜索树的后序遍历序列,题目:输入一个整数数组,判断该数组是不是
   某二叉搜索树的后序遍历结果。如果是返回True, 不是返回False,假设输入
   的两个数组的任意两个数字都不相同。分析:特殊的地方是二叉搜索树是具有
   排序属性的(左子树小于根节点,根节点小于右子树),加上后序遍历,故我们
   需要分析出它的背后的规律。在后序遍历中,最后一个数字是树的根节点的值,
   数组中前面的数字可以分为两部分,第一部分是左子树节点的值,他们都比根
   节点小;第二部分是右子节点的值,它们都比根节点大。接下来用同样的方法,
   确定于数组每一部分对应子树的结构。这其实是一个递归的过程。
*/

// 找到规律后再写代码,就容易多了。
bool verifySquenceOfBST(int sequence[],int length)
{
    if(sequence == NULL || length <= 0)
        return false;
    
    // 后序遍历最后一个节点就是根节点
    int root = sequence[length-1];
    
    //找到左子树的序列,在二叉搜索树中,左子树的节点小于根节点
    int i = 0;
    for(; i< length-1; ++i)
    {
        if(sequence[i] > root)
            break;
    }

    //在二叉搜索树中,右子树的节点大于根节点
    int j = i;
    for(; j<length-1; ++j)
    {
        if(sequence[j] < root)
            return false;
    }
    // 判断左子树是不是二叉搜索树
    bool left = true;
    if(i>0)
        left = verifySquenceOfBST(sequence,i);
    // 判断右子树是不是二叉搜索树
    bool right = true;
    if(i < length-1)
        right = verifySquenceOfBST(sequence+i, length - i +1);
    
    return left && right;
}


/* 
7、二叉树中和为某一个值的路径,题目:输入一个二叉树和一个整数,打印出二叉树中节点值的和为
   输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。事例,如
   下图,对于树的路径和这样一个新概念,很难一下想到完整的思路,故可以先从一两个具体的例子
   入手,寻找规律。首先加和是从根到叶子,故即遍历中,只有前序遍历是先根节点,故应该是前序遍历
   ,其次具体思路可也参见下图说明。
   
   这个题明显是所谓的回溯搜索的过程,参见 labuladong 算法小抄。此外应为要输出整条路径,所以
   需要将之前遍历过得节点保存下来,这里采用 vector. 在遍历完,不满足条件后需要回溯,即进行逆操作。
*/

                 

                  

                   

                     

                 

void findPath(BiTreeNode* pRoot,int expectedSum)
{
    if(pRoot == NULL)
        return;
    // 用 vector 模拟 stack
    std::vector<int> path;
    int currentSum = 0;
    FindPath(pRoot,expectedSum,path,currentSum);
}

void FindPath(BiTreeNode* pRoot,int expectedSum,std::vector<int>& path,int &currentSum)
{
    // 加入根节点的值,压入栈
    currentSum += pRoot->m_value;
    path.push_back(pRoot->m_value);

    // 如果是叶节点,并且路径上的叶节点上值之和等于输入的值,则打印出这条路径
    bool isLeaf = pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL;
    if(currentSum == expectedSum && isLeaf)
    {
        printf("The path is Find: ");
        std::vector<int>::iterator iter = path.begin();
        for(; iter < path.end(); ++iter)
        {
            print("%d\t", *iter);
        }
        printf("\n");
    }

    // 如果不是叶节点就遍历它的子节点
    if(pRoot->m_pLeft != NULL)
        FindPath(pRoot->m_pLeft,expectedSum,path,currentSum);
    if(pRoot->m_pRight != NULL)
        FindPath(pRoot->m_pRight,expectedSum,path,currentSum);

    // 在返回父节点的时候,在路径上删除当前节点,并在 currentSum 中减去当前的值
    currentSum -= pRoot->m_value;
    path.pop_back();
}


/* 
8、求树中两个结点的最低公共祖先。注:由于这里涉及到树的类型,不同的类型有不同的解法,
   这里就默认是最为普通的树。如果是二叉搜索树,则是比较方便的。因为二叉搜索树是排过
   序的,如果当前节点比输入的两个节点的值都大,说明两个节点都在左子树,反之,在右子树
   。若在左边,则下一步遍历左子节点,若在右边,则遍历右子节点。依次递归遍历直到找到两
   个第一个在两个节点值之间的节点,即为最低公共祖先。如果不是二叉搜索树,是一般的树,
   且二叉树都不是,但是有指向父节点的指针。则可以将其中一条路径转换为从输入节点到根
   节点的链表,此时问题就可以转化为两个链表求取第一个公共子节点的问题。如果也没有指
   向父节点的指针,就是最为普通的树。则可以采用辅助空间存储从根节点到输入节点的链表
   ,然后在求取最后公共子节点的问题。算法如下:
*/
bool GetNodePath(TreeNode* pRoot,TreeNode* pNode,std::list<TreeNode* > & path)
{
    if(pRoot == pNode)
        return true;
    path.push_back(pRoot);

    bool found = false;
    std::vector<TreeNode* >::iterator i = pRoot->m_vChildrne.begin();
    while(!found && i<pRoot->m_vChildrne.end())
    {
        found = GetNodePath(*i,pNode,path);
        ++i;
    }
    if(!found)
        path.pop_back();
    
    return found;
}


TreeNode* getLastCommonNode(const std::list<TreeNode* > &path1,const std::list<TreeNode* > & path2)
{
    std::list<TreeNode* >::const_iterator iter1 = path1.begin();
    std::list<TreeNode* >::const_iterator iter2 = path2.begin();

    TreeNode* pLastNode = NULL;

    while(iter1 != path1.end() && iter2 != path2.end())
    {
        if(*iter1 == *iter2)
            pLastNode = *iter1;
        
        ++ iter1;
        ++ iter2;
    }
    return pLastNode;
}

TreeNode* getLastCommonParent(TreeNode* pRoot,TreeNode* pNode1,TreeNode* pNode2)
{
    if(pRoot == NULL || pNode1 == NULL || pNode2 == NULL)
        return NULL;
    
    std::list<TreeNode* > path1;
    GetNodePath(pRoot,pNode1,path1);

    std::list<TreeNode* > path2;
    GetNodePath(pRoot,pNode2,path2);

    return GetLastCommonNode(path1,path2);
}


/* 
9、求最小的 K 个数,若果不能改变数组顺序,建议直接使用堆结构(适合处理海量数据)。
   如 stl 里面的优先级队列 priority_queue,或者使用红黑树结构的 set、map 等。
   这里使用 set 
*/
typedef std::multiset<int, std::greater<int> >  intSet;
typedef std::multiset<int, std::greater<int> >::iterator  setIterator;

void GetLeastNumber(const std::vector<int> & data,intSet & leastNumbers,int k)
{
    leastNumbers.clear();

    if(k<1 || k > data.size())
        return;
    
    vector<int>::const_iterator iter = data.begin();
    for(; iter != data.end(); ++iter)
    {
            // 先插入 k 个值
        if((leastNumbers.size()) < k)
            leastNumbers.intsert(*iter);
        else 
        {   // 然后在调整
            setIterator iterGreastest = leastNumbers.begin();
            if(*iter < *(leastNumbers.begin()))
            {
                leastNumbers.erase(iterGreastest);
                leastNumbers.insert(*iter);
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

smilejiasmile

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值