微软面试题学习心得1

20 篇文章 0 订阅
20 篇文章 0 订阅

声明:

这篇博客的某些题目和答案成果源自于July和何海涛的博客,网址:点击打开链接   点击打开链接

本人只是针对自己情况,把感兴趣的题目都罗列出来;针对其他的题目(不是来自上面两位),写出了自己的算法,仅供自己慢慢学习和品味。如有问题,请在博客下面留言。


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


struct BSTreeNode
{
	int m_nValue; // value of node
	BSTreeNode *m_pLeft; // left child of node
	BSTreeNode *m_pRight; // right child of node
};
typedef BSTreeNode DoubleList;
DoubleList * pHead;
DoubleList * pListIndex;
void convertToDoubleList(BSTreeNode * pCurrent);

// 创建二元查找树
void addBSTreeNode(BSTreeNode * & pCurrent,  int value)
{
	if (NULL == pCurrent)
	{
		BSTreeNode * pBSTree = new BSTreeNode();
		pBSTree->m_pLeft = NULL;
		pBSTree->m_pRight = NULL;
		pBSTree->m_nValue = value;
		pCurrent = pBSTree;
	}
	else 
	{
		if ((pCurrent->m_nValue) > value)
		{
			addBSTreeNode(pCurrent->m_pLeft, value);
		}
		else if ((pCurrent->m_nValue) < value)
		{
			addBSTreeNode(pCurrent->m_pRight, value);
		}
		else
		{
			//cout<<"重复加入节点"<<endl;
		}
	}
}


// 遍历二元查找树 中序
void ergodicBSTree(BSTreeNode * pCurrent)
{
	if (NULL == pCurrent)
	{ 
		return;
	}
	if (NULL != pCurrent->m_pLeft)
	{
		ergodicBSTree(pCurrent->m_pLeft); 
	}
	// 节点接到链表尾部
	convertToDoubleList(pCurrent);
	// 右子树为空
	if (NULL != pCurrent->m_pRight)
	{
		ergodicBSTree(pCurrent->m_pRight);
	}
}

// 二叉树转换成list
void convertToDoubleList(BSTreeNode * pCurrent)
{
	pCurrent->m_pLeft = pListIndex;
	if (NULL != pListIndex)
	{
		pListIndex->m_pRight = pCurrent;
	}
	else
	{
		pHead = pCurrent;
	} 
	pListIndex = pCurrent;
	cout<<pCurrent->m_nValue<<endl;
}



2.设计包含 min 函数的栈。

定义栈的数据结构,要求添加一个 min  函数,能够得到栈的最小元素。 要求函数 min、push 以及 pop  的时间复杂度都是 O(1)。

结合链表一起做。

首先我做插入以下数字:10,7,3,3,8,5,2, 6
0: 10 -> NULL (MIN=10, POS=0)
1: 7 -> [0] (MIN=7, POS=1) 用数组表示堆栈,第0个元素表示栈底
2: 3 -> [1] (MIN=3, POS=2)
3: 3 -> [2] (MIN=3, POS=3)
4: 8 -> NULL (MIN=3, POS=3) 技巧在这里,因为8比当前的MIN大,所以弹出8不会对当前的MIN产生影响
5:5 -> NULL (MIN=3, POS=3)
6: 2 -> [2] (MIN=2, POS=6) 如果2出栈了,那么3就是MIN
7: 6 -> [6]

出栈的话采用类似方法修正。


所以,此题的第1小题,即是借助辅助栈,保存最小值,
且随时更新辅助栈中的元素。
如先后,push 2 6 4 1 5
stack A  stack B(辅助栈)

4:  5       1      //push 5,min=p->[3]=1     ^
3:  1       1      //push 1,min=p->[3]=1     |   //此刻push进A的元素1小于B中栈顶元素2
2:  4       2      //push 4,min=p->[0]=2     |
1:  6       2      //push 6,min=p->[0]=2     |
0:  2       2      //push 2,min=p->[0]=2     |

push第一个元素进A,也把它push进B,
当向Apush的元素比B中的元素小,  则也push进B,即更新B。否则,不动B,保存原值。
向栈A push元素时,顺序由下至上。
辅助栈B中,始终保存着最小的元素。

然后,pop栈A中元素,5 1 4 6 2
     A       B ->更新 
4:   5       1    1     //pop 5,min=p->[3]=1      |
3:   1       1    2     //pop 1,min=p->[0]=2      |
2:   4       2    2     //pop 4,min=p->[0]=2      |
1:   6       2    2     //pop 6,min=p->[0]=2      |
0:   2       2    NULL  //pop 2,min=NULL          v

当pop A中的元素小于B中栈顶元素时,则也要pop B中栈顶元素。


struct MinStackElement {
	int data;
	int min;
};

struct MinStack {
	MinStackElement * data; 
	int size;
	int top;
}

MinStack MinStackInit(int maxSize) { 
	MinStack stack;
	stack.size = maxSize;
	stack.data = (MinStackElement*) malloc(sizeof(MinStackElement)*maxSize);
	stack.top = 0;
	return stack;
}

void MinStackFree(MinStack stack){
	free(stack.data);
}

void MinStackPush(MinStack stack, int d) {
	if (stack.top == stack.size) 
		error(“out of stack space.”); 
	MinStackElement* p = stack.data[stack.top];
	p->data = d;
	p->min = (stack.top==0?d : stack.data[top-1]);
	if (p->min > d) p->min = d;
	top ++;
}
int MinStackPop(MinStack stack) {
	if (stack.top == 0) 
		error(“stack is empty!”);
	return stack.data[--stack.top].data;
}
int MinStackMin(MinStack stack) {
	if (stack.top == 0) 
		error(“stack is empty!”);
	return stack.data[stack.top-1].min;
}


3.在二元树中找出和为某一值的所有路径 题目:输入一个整数和一棵二元树。 从树的根结点开始往下访问一直到叶结点所经过的所有结点形成一条路径。打印出和与输入整数相等的所有路径。

例如输入整数22 和如下二元树

10

/ \

5 12

/ \

4 7


则打印出两条路径:10, 12 和10, 5, 7


///
// Find paths whose sum equal to expected sum
///
void FindPath
(
      BinaryTreeNode*   pTreeNode,    // a node of binary tree
      int               expectedSum,  // the expected sum
      std::vector<int>& path,         // a path from root to current node
      int&              currentSum    // the sum of path
)
{
      if(!pTreeNode)
            return;

      currentSum += pTreeNode->m_nValue;
      path.push_back(pTreeNode->m_nValue);

      // if the node is a leaf, and the sum is same as pre-defined, 
      // the path is what we want. print the path
      bool isLeaf = (!pTreeNode->m_pLeft && !pTreeNode->m_pRight);
      if(currentSum == expectedSum && isLeaf)
      {    
           std::vector<int>::iterator iter = path.begin();
           for(; iter != path.end(); ++ iter)
                 std::cout << *iter << '\t';
           std::cout << std::endl;
      }

      // if the node is not a leaf, goto its children
      if(pTreeNode->m_pLeft)
            FindPath(pTreeNode->m_pLeft, expectedSum, path, currentSum);
      if(pTreeNode->m_pRight)
            FindPath(pTreeNode->m_pRight, expectedSum, path, currentSum);

      // when we finish visiting a node and return to its parent node,
      // we should delete this node from the path and 
      // minus the node's value from the current sum
      currentSum -= pTreeNode->m_nValue;
      path.pop_back();
} 

 



4   假设你有一个用1001 个整数组成的数组,这些整数是任意排列的,但是你知道所有的整数都在1  到

1000(包括1000)之间。此外,除一个数字出现两次外,其他所有数字只出现一次。假设你只能对这个数组做一次处理,用一种算法找出重复的那个数字。如果你在运算中使用了辅助的存储方式,那么你能找到不用这种

方式的算法吗?

ANSWER:

Sum up all the numbers, then subtract the sum from 1001*1002/2.Another way, use A XORA XOR B = B:

int findX(int a[]) {
int k = a[0];
for (int i=1; i<=1000;i++)
k ~= a[i]~i;
}
return k;


5  不用乘法或加法增加8 倍。现在用同样的方法增加7倍。

ANSWER: 

n<<3;

(n<<3)-n;


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

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

8

/\

610

/\ / \

57 9 11


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

using namespace std;

///
// Verify whether a squence of integers are the post order traversal
// of a binary search tree (BST)
// Input: squence - the squence of integers
//        length  - the length of squence
// Return: return ture if the squence is traversal result of a BST,
//         otherwise, return false
///
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);
}


7 题目:求1+2+…+n,

要求不能使用乘除法、for、while、if、else、switch、case 等关键字以及条件判断语句  

分析:这道题没有多少实际意义,因为在软件开发中不会有这么变态的限制。但这道题却能有效地考查发散思维能力,而发散思维能力能反映出对编程相关技术理解的深刻程度。

通常求1+2+…+n除了用公式n(n+1)/2之外,无外乎循环和递归两种思路。由于已经明确限制forwhile的使用,循环已经不能再用了。同样,递归函数也需要用if语句或者条件判断语句来判断是继续递归下去还是终止递归,但现在题目已经不允许使用这两种语句了。

我们仍然围绕循环做文章。循环只是让相同的代码执行n遍而已,我们完全可以不用forwhile达到这个效果。比如定义一个类,我们new一含有n个这种类型元素的数组,那么该类的构造函数将确定会被调用n次。我们可以将需要执行的代码放到构造函数里。如下代码正是基于这个思路:


class Temp
{
public:
      Temp() { ++ N; Sum += N; }

      static void Reset() { N = 0; Sum = 0; }
      static int GetSum() { return Sum; }

private:
      static int N;
      static int Sum;
};

int Temp::N = 0;
int Temp::Sum = 0;

int solution1_Sum(int n)
{
      Temp::Reset();

      Temp *a = new Temp[n];
      delete []a;
      a = 0;

      return Temp::GetSum();
}


8

题目:输入一个单向链表,输出该链表中倒数第 k 个结点。链表的倒数第0  个结点为链表的尾指针。 

链表节点定义如下:

struct ListNode
{
      int       m_nKey;
      ListNode* m_pNext;
};

分析:为了得到倒数第k个结点,很自然的想法是先走到链表的尾端,再从尾端回溯k步。可是输入的是单向链表,只有从前往后的指针而没有从后往前的指针。因此我们需要打开我们的思路。

既然不能从尾结点开始遍历这个链表,我们还是把思路回到头结点上来。假设整个链表有n个结点,那么倒数第k个结点是从头结点开始的第n-k-1个结点(从0开始计数)。如果我们能够得到链表中结点的个数n,那我们只要从头结点开始往后走n-k-1步就可以了。如何得到结点数n?这个不难,只需要从头开始遍历链表,每经过一个结点,计数器加一就行了。

这种思路的时间复杂度是O(n),但需要遍历链表两次。第一次得到链表中结点个数n,第二次得到从头结点开始的第n?-k-1个结点即倒数第k个结点。

如果链表的结点数不多,这是一种很好的方法。但如果输入的链表的结点个数很多,有可能不能一次性把整个链表都从硬盘读入物理内存,那么遍历两遍意味着一个结点需要两次从硬盘读入到物理内存。我们知道把数据从硬盘读入到内存是非常耗时间的操作。我们能不能把链表遍历的次数减少到1?如果可以,将能有效地提高代码执行的时间效率。

如果我们在遍历时维持两个指针,第一个指针从链表的头指针开始遍历,在第k-1步之前,第二个指针保持不动;在第k-1步开始,第二个指针也开始从链表的头指针开始遍历。由于两个指针的距离保持在k-1,当第一个(走在前面的)指针到达链表的尾结点时,第二个指针(走在后面的)指针正好是倒数第k个结点。

这种思路只需要遍历链表一次。对于很长的链表,只需要把每个结点从硬盘导入到内存一次。因此这一方法的时间效率前面的方法要高。

///
// Find the kth node from the tail of a list
// Input: pListHead - the head of list
//        k         - the distance to the tail
// Output: the kth node from the tail of a list
///
ListNode* FindKthToTail_Solution2(ListNode* pListHead, unsigned int k)
{
      if(pListHead == NULL)
            return NULL;

      ListNode *pAhead = pListHead;
      ListNode *pBehind = NULL;

      for(unsigned int i = 0; i < k; ++ i)
      {
            if(pAhead->m_pNext != NULL)
                  pAhead = pAhead->m_pNext;
            else
            {
                  // if the number of nodes in the list is less than k, 
                  // do nothing
                  return NULL;
            }
      }

      pBehind = pListHead;

      // the distance between pAhead and pBehind is k
      // when pAhead arrives at the tail, p
      // Behind is at the kth node from the tail
      while(pAhead->m_pNext != NULL)
      {
            pAhead = pAhead->m_pNext;
            pBehind = pBehind->m_pNext;
      }

      return pBehind;
}


题目:输入一个已经按升序排序过的数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。要求时间复杂度是O(n)。如果有多对数字的和等于输入的数字,输出任意一对即可。

例如输入数组12471115和数字15。由于4+11=15,因此输出411

///
// Find two numbers with a sum in a sorted array
// Output: ture is found such two numbers, otherwise false
///
bool FindTwoNumbersWithSum
(
      int data[],           // a sorted array
      unsigned int length,  // the length of the sorted array     
      int sum,              // the sum
      int& num1,            // the first number, output
      int& num2             // the second number, output
)
{

      bool found = false;
      if(length < 1)
            return found;

      int ahead = length - 1;
      int behind = 0;

      while(ahead > behind)
      {
            long long curSum = data[ahead] + data[behind];

            // if the sum of two numbers is equal to the input
            // we have found them
            if(curSum == sum)
            {
                  num1 = data[behind];
                  num2 = data[ahead];
                  found = true;
                  break;
            }
            // if the sum of two numbers is greater than the input
            // decrease the greater number
            else if(curSum > sum)
                  ahead --;
            // if the sum of two numbers is less than the input
            // increase the less number
            else
                  behind ++;
      }

      return found;
}


9 题目:输入一颗二元查找树,将该树转换为它的镜像,即在转换后的二元查找树中,左子树的结点都大于右子树的结点。用递归和循环两种方法完成树的镜像转换。

例如输入:

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

输出:

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

定义二元查找树的结点为:

struct BSTreeNode // a node in the binary search tree (BST)
{
      int          m_nValue; // value of node
      BSTreeNode  *m_pLeft;  // left child of node
      BSTreeNode  *m_pRight; // right child of node
};

分析:尽管我们可能一下子不能理解镜像是什么意思,但上面的例子给我们的直观感觉,就是交换结点的左右子树。我们试着在遍历例子中的二元查找树的同时来交换每个结点的左右子树。遍历时首先访问头结点8,我们交换它的左右子树得到:

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

我们发现两个结点610的左右子树仍然是左结点的值小于右结点的值,我们再试着交换他们的左右子树,得到:

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

刚好就是要求的输出。

上面的分析印证了我们的直觉:在遍历二元查找树时每访问到一个结点,交换它的左右子树。这种思路用递归不难实现,将遍历二元查找树的代码稍作修改就可以了。参考代码如下:


///
// Mirror a BST (swap the left right child of each node) recursively
// the head of BST in initial call
///
void MirrorRecursively(BSTreeNode *pNode)
{
      if(!pNode)
            return;

      // swap the right and left child sub-tree
      BSTreeNode *pTemp = pNode->m_pLeft;
      pNode->m_pLeft = pNode->m_pRight;
      pNode->m_pRight = pTemp;

      // mirror left child sub-tree if not null
      if(pNode->m_pLeft)
            MirrorRecursively(pNode->m_pLeft);  

      // mirror right child sub-tree if not null
      if(pNode->m_pRight)
            MirrorRecursively(pNode->m_pRight); 
}


10   题目:输入一颗二元树,从上往下按层打印树的每个结点,同一层中按照从左往右的顺序打印。

例如输入

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

输出8   6   10   5   7   9   11。

分析:这曾是微软的一道面试题。这道题实质上是要求遍历一棵二元树,只不过不是我们熟悉的前序、中序或者后序遍历。

我们从树的根结点开始分析。自然先应该打印根结点8,同时为了下次能够打印8的两个子结点,我们应该在遍历到8时把子结点610保存到一个数据容器中。现在数据容器中就有两个元素10了。按照从左往右的要求,我们先取出6访问。打印6的同时要把6的两个子结点57放入数据容器中,此时数据容器中有三个元素1057。接下来我们应该从数据容器中取出结点10访问了。注意1057先放入容器,此时又比57先取出,就是我们通常说的先入先出。因此不难看出这个数据容器的类型应该是个队列

如果我们能知道每层的最后一个结点,那么就方便多了,输出每层最后一个结点的同时,输出一个换行符。因此,关键在于如何标记每层的结束。可以考虑在每层的最后一个点之后,插入一个空结点。比如队列中先放入根结点,由于第0层只有一个结点,因此放入一个空结点。然后依次取出队列中的结点,将其子女放入队列中,如果遇到空结点,表明当前层的结点已遍历完了,而队列中放的恰恰是下一层的所有结点。如果当前队列为空,表明下一层无结点,也就说是所有结点已遍历好了。如果不为空,那么插入一个空结点,用于标记下一层的结束。

void LevelReverse(BSTreeNode *pRoot)  
{  
    if(pRoot == NULL)  
        return;  
    queue<BSTreeNode *> nodeQueue;    
    nodeQueue.push(pRoot);    
    nodeQueue.push(NULL);   //放入空结点,作为层的结束符  
    while(nodeQueue.size())  
    {  
        BSTreeNode * pNode = nodeQueue.front(); //取队首元素    
        nodeQueue.pop(); //必须出队列   
        if(pNode)  
        {  
            if(pNode->left)  //左子女    
                nodeQueue.push(pNode->left);    
            if(pNode->right) //右子女    
                nodeQueue.push(pNode->right);  
            cout<<pNode->value<<' ';    

        }  
        else if(nodeQueue.size()) //如果结点为空并且队列也为空,那么所有结点都已访问  
        {  
            nodeQueue.push(NULL);  
            cout<<endl;  
        }  
    }  
}  


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值