[微软面试100题]1-10

第一题:把二分查找树转换为升序排序的双向链表。不能新建节点,只能改变树的指针。

二分查找树:左小于父,右大于父。中序遍历就是升序遍历。
中序遍历递归法:
void showMidTree(BSTreeNode *pRoot)
{
    if(pRoot!=NULL)
    {
        showMidTree(pRoot->m_pLeft);
        cout<<pRoot->m_nValue<<endl;
        showMidTree(pRoot->m_pRight);
    }
}

思路一:当我们到达某一结点准备调整以该结点为根结点的子树时,先调整其左子树将左 子树转换成一个排好序的左子链表,再调整其右子树转换右子链表。最近链接左子链表的最右结点(左子树的最大结点)、当前结点和右子链表的最左结点(右子树 的最小结点)。从树的根结点开始递归调整所有结点。

算法复杂度:中序遍历的复杂度为O(n)
 
通过新建一个vector再输出值的地方存储找到的指针,这样存储的顺序为升序。然后把指针连接为双向链表。
struct BSTreeNode{
    BSTreeNode(){
        m_pLeft=NULL;
        m_pRight=NULL;
    }
    bool legal(set<BSTreeNode*> &found);
    int m_nValue;
    BSTreeNode *m_pLeft;
    BSTreeNode *m_pRight;
};


 
//递归建立二分查找树
BSTreeNode* buildTree(int *num,int count){
    BSTreeNode *pRoot=new BSTreeNode;
    for(int i=0;i<count;++i){
        if(i==0){
            pRoot->m_nValue=num[i];
        }
        else{
            BSTreeNode *pNode=new BSTreeNode;
            BSTreeNode *pTmp=pRoot;
            while(pTmp!=NULL){
                //比父节点大的分配到右边
                if(pTmp->m_nValue<=num[i]){
                    if(pTmp->m_pRight==NULL){
                        pTmp->m_pRight=pNode;
                        pTmp->m_pRight->m_nValue=num[i];
                        break;
                    }
                    else{
                        pTmp=pTmp->m_pRight;
                    }
                }
                else{
                    if(pTmp->m_pLeft==NULL){
                        pTmp->m_pLeft=pNode;
                        pTmp->m_pLeft->m_nValue=num[i];
                        break;
                    }
                    else{
                        pTmp=pTmp->m_pLeft;
                    }
                }
            }
        }
    }
    return pRoot;
}
 
//中序遍历 就是二分查找树的升序遍历。
void showMidTree(BSTreeNode *pRoot){
    if(pRoot!=NULL){
        showMidTree(pRoot->m_pLeft);
        cout<<pRoot->m_nValue<<endl;
        showMidTree(pRoot->m_pRight);
    }
}
 
void connect(BSTreeNode *pRoot,BSTreeNode* &p){
    if(pRoot!=NULL){
        connect(pRoot->m_pLeft,p);
        //p初值为NULL,之后记录前一个值的指针
        if(p!=NULL){
            pRoot->m_pLeft=p;
            p->m_pRight=pRoot;
            p=pRoot;
        }
        else{
            p=pRoot;
        }
        connect(pRoot->m_pRight,p);
    }
 
}
 
BSTreeNode* tree2list(BSTreeNode *pRoot){
    BSTreeNode *p=NULL;
    connect(pRoot,p);
    while(pRoot->m_pLeft!=NULL){
        pRoot=pRoot->m_pLeft;
    }
    return pRoot;
}
 
int main()
{
    int num[]={10,6,14,4,8,12,16};
    BSTreeNode *pRoot=buildTree(num,7);
 
    BSTreeNode *start=tree2list(pRoot);
    while(start!=NULL){
        cout<<start->m_nValue<<endl;
        start=start->m_pRight;
    }
}
思路二:我们可以中序遍历整棵树。按照这个方式遍历树,比较小的结点先访问。如果我们每访问一个结点,假设之前访问过的结点已经调整成一个排序双向链表,我们再把调整当前结点的指针将其链接到链表的末尾。当所有结点都访问过之后,整棵树也就转换成一个排序双向链表了。

貌似答案就是这个思路,还没做出来

 
第二题:实现一个带功能min的栈。push() pop() min()的时间复杂度都为O(1)
得到min复杂度为1,需要使用到动态规划的思想。 min Stack[n]=(val[n]>min Stack[n-1] ? min Stack[n-1] : val)
意思是说,当栈push一个元素的时候,如果新元素大于没push时栈的最小值,则最小值维持不变。否则新的元素为最小值。
每一个元素用一个结构体记录当时的值,和到当前位置的栈的最小值。这样当pop时就可以马上以O(1)得到pop后新栈的最小值了
struck minStackElem
{ int val;  int minVal;//从栈底到此元素的最小值};

第三题:求最大子数组和,使用动态规划复杂度为O(n),题目同编程之美一样

解法1(不提供最大数组区间,值提供最大值):max初始为0,从头累加数组,如果sum>max则max=sum,通过此方法来记录最大和。因为当遇到负数sum下降时,max是不作记录的。 但当sum下降到负数时,最大和就为0(区间大小为0,不作区间和已经为0已经为最大了),负数肯定比0小,所以已经肯定不是最大和了,因此赋值sum=0。
解法2(编程之美解法,详细见另外的日志):把数组分割为前半部分和最后一个元素,数组的最大和分为3种情况,1是最大和区间存在于前半段(不包括最后一个元素的段),2是最大和从最后一个元素开始,3是最大和从最后一个元素之后的元素开始。

第四题:二元树中找出和为某一值的所有路径

采用递归的思想,函数递归调用子节点,并把目标数值改为原始目标-本节点值作为新目标。递归函数还有一个队列参数(非引用),相当于每次递归调用都复制一次队列,这样就确保每一条路径有一个队列来保存,不会混乱。每次经过节点就把节点值打入队列。如发现函数的目标==当前节点值,则说明已经找到一条路径。打印路径的方法是把队列的元素依次打印即可。
void BinaryTreeNode::getPath(int thisSum,int sum,deque<int> deq){
    deq.push_back(this->m_nValue);
    //find it
    if(thisSum==this->m_nValue){
        cout<<"find:"<<endl;
        while(deq.size()>0){
            cout<<deq.front()<<endl;
            deq.pop_front(); }
        return; }
if(this->m_nValue<thisSum){
        if(this->m_pLeft!=NULL){
            this->m_pLeft->getPath(thisSum-this->m_nValue,sum,deq);
        }
        if(this->m_pRight!=NULL){
            this->m_pRight->getPath(thisSum-this->m_nValue,sum,deq);
        }
}
}

第五题:寻找数组中最小的K个元素

采用局部堆排序,时间复杂度为O(nlogk)。
堆:父节点总比子节点要大(小),插入数据后调整堆的复杂度为logk。

最大堆原理解释:http://blog.csdn.net/xiaoxiaoxuewen/article/details/7570621

 第七题:判断单链表是否相交,是否有环
判断有环:指针slow步进为1,指针fast步进为2,如果链表有环,则两指针将会在环内相遇。
方法一(链表有无环都适用):把链表1的节点地址存入hashtable,遍历链表2判断地址是否在hashtable中。时间复杂度为O(n1+n2)
方法二:(无环适用):如两单链表相交,则从交点到最后的节点都是重复的。因此找到两链表的最后节点对比即可。复杂度同上。
方法三:(适用有环):链表一步长为1前进,链表二步长2前进,如两链表相交则必然相遇。(因为会汇集到同一环上)。

于此,还得好好总结下,问题得这样解决:

1.先判断带不带环

2.如果都不带环,就判断尾节点是否相等

3.如果都带环,那么一个指针步长为1遍历一链表,另一指针,步长为2,遍历另一个链表。

但第3点,如果带环 但不相交,那么程序会陷入死循环。。

第八题:颠倒单向链表顺序

单向链表建立与释放:
//在析构函数中调用下个个节点的delete就可以只在外层delete头节点就可以连锁释放所有节点了。
template<class T>
link<T>::~link(){
    if(this->next!=NULL){
        delete this->next;
    }
    cout<<this->val<<" has been released"<<endl;
}
 
template<class T>
void link<T>::append(T val){
    link<T> *tmp=this;
    while(tmp->next!=NULL){
        tmp=tmp->next;
    }
    tmp->next=new link<T>;
    tmp->next->val=val;
    cout<<tmp->next->val<<" is appended"<<endl;
}

递归方法:

//技巧:使用参数传递所需的前值(以前我一般喜欢用返回值),
//使用返回值传递结果(结果通常在最底层),如现在的反向链表的头指针
template<class T>
link<T>* link<T>::reverse(link<T> *rnext){
    if(this->next!=NULL){
        link<T>* head=this->next->reverse(this);
        this->next=rnext;
        return head;
    }
    this->next=rnext;
    return this;
}

非递归方法:

template<class T>
link<T>* link<T>::reverseUnrecursive(){
    link<T> *previous=NULL,*head=this,*pNext=this->next;
    while(head!=NULL){
        pNext=head->next;
        head->next=previous;
        previous=head;
        head=pNext;
    }
    return previous;
}

第八题:通用字符串匹配-非递归法-可输出第一个匹配的子串

?代表1一个字符,*代表0个或任意个,其他字符严格匹配
方法:对格式串从头遍历,根据格式串的字符分情况讨论。1、当为? 2、当为* 3、当为普通字符 4、当匹配失败
输出匹配段方法:根据match函数返回的从第iMatch个字符开始匹配,根据模式打印出来,详细见代码。
开发方法:先写测试函数,包含多个测试用例覆盖所有边界条件。每次修改都运行测试函数确保满足所有情况。
 
第八题:通用字符串匹配-递归法-只能判断是否完全匹配。
比如模式是a?,它不跟abc完全匹配,只跟ab完全匹配,但用上面的方法就会返回iMatch=0,因为abc中的子串ab与a?匹配。

跟上面一样分4种情况进行递归即可,详细看源码。我做的方法与答案的不同,但更好理解。采用先编写测试用例再编程确实非常高效!(回归测试

 第八题:翻转字符串
方法:从头和从尾同时向中间逼近,每走一步调换头尾字符即可。复杂度仅为O(n/2)。PS:char *p="abc",p是const不能改变字符串内容

第八题:翻转句子的词

方法:先用上面的翻转字符串的函数把整个句子反转了。然后再用上面的函数逐个单词反转。为了能把上面函数使用到单词上,函数需要传递一个参数,单词或字符串的长度,而不是像平常一样用\0来判断字符串结束。这样就可以运用到单词上了。

第八题:查找子字符串

方法1:遍历字符串,如其中字母与子字符串的首字母相同,则再判断后来的是否与子串完全相同,是则找到。不是则继续往下遍历。
方法2:把字符串建立字典树。然后在字典树中查找子串对应的分支是否有与子串一样的。复杂度为O(k),k为子串长度。

第八题:比较字符串,要求O(n)时间复杂度和O(1)空间复杂度

一一对比,不符则跳出返回即可。注意编程简洁风格。单行的if应直接写后面不写括号

第八题:一个长为1001的数组中乱序存放1到1000的整数,除一个数外其余都不重复。只可以遍历数组一次且不可使用辅助存储,找出重复。

方法1:数组和-sum(1,1000)=重复数
方法2:利用 A^A^B=B的性质。将k初始为0,k^=(1到100)再^=(数组各个元素)。由于这样1~1000每个都异或了2次相互抵消了,剩下的就是异或了3次的重复数。
int a[]={1,5,2,3,4,5};
    int k=0;
    for(int i=0;i<6;++i){
        k^=a[i];
        if(i<5)k^=i+1;
    }
    cout<<k<<endl;

异或的性质:同则0- A^A=0 不同则1- A^B=1;异或0不影响A^0=A;因此初始化为0.

注意:异或^   按位取反~,   k与b取异或 k^=b     k取反~k

第八题:不用乘法和加法,运算一个整数的八倍,七倍

左移一位相当于乘以2,八倍:k<<3  七倍:(k<<3)-k
注意:无论左移右移,移动的位数都放在右操作数

第九题:判断整数序列是否是二元查找树后序遍历的结果

序遍历:-左-右 访问中,再访问左子树,最后右子树。
序遍历:左-右-
序遍历:左--右

二元查找树中序:从小到大; 后序:最后的元素为中间值,前半段元素小于中间值,后半段大于中间值

利用二元查找树后序的性质,可根据数组的下标把数组分段,然后使用递归。
int isPostOrder(int *a,int start,int end){
    if(start==end)return 1;
    int pivot=a[end];//最后的元素是树根,前半段小于根,后半段大于根
    int mid=(end+start)/2;
    int i,j;
    for(i=start;i<mid;++i){
        if(a[i]>pivot)return 0;
    }
    for(j=end-1;j>=mid;--j){
        if(a[j]<pivot)return 0;
    }
    if(i!=j+1)return 0;//判断是否对称,是否二元树
    return isPostOrder(a,start,mid-1) && isPostOrder(a,mid,end-1);
}
posted @ 2013-03-28 09:40  iyjhabc  阅读(461)  评论(0编辑  收藏  举报