《剑指Offer》题一~题十

一、赋值运算符函数

题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数。

class CMyString {
public:
	CMyString(char *pData = nullptr);
	CMyString(const CMyString &str);
	~CMyString();
private:
	char *m_pData;
}; 

测试用例:

  • 把一个CMyString的实例赋值给另外一个实例。
  • 把一个CMyString的实例赋值给它自己。
  • 连续赋值。

没有考虑异常安全性的解法:

CMyString& CMyString::operator=(const CMyString &rhs)
{
	if(this != &rhs) 
	{
		delete []m_pData;
		m_pData = nullptr;
		m_pData = new char[strlen(rhs.m_pData) + 1];
		strcpy(m_pData, rhs.m_pData);
	} 
	return *this;
}

考虑异常安全性的解法:

CMyString& CMyString::operator=(const CMyString &rhs)
{
	if(this != &rhs)
	{ 
		CMyString strTemp(rhs);			// 创建一个临时实例strTemp 
		/* 交换strTemp.m_pData和实例自身的m_pData */ 
		char *pTemp = m_pData;			
		m_pData = strTemp.m_pData;
		strTemp.m_pData = pTemp;
	}   /* 自动调用strTemp的析构函数,释放strTemp.m_pData指向的新内存 */
	return *this;
}

补:在新的代码中,我们在CMyString的构造函数里用new分配内存,故如果内存不足将抛出诸如bad_alloc等异常,但由于我们还没有修改实例自身的状态,故实例的状态还是有效的,这也就保证了异常安全性。

考点:

  • C++基础语法,如运算符函数、常量引用等。
  • 内存泄露。
  • 代码异常安全性。

 

二、实现Singleton(单例)模式

题目:设计一个类,我们只能生成该类的一个实例。

解读题意:只能生成一个实例的类是实现了Singleton模式的类型。

考点:

  • 单例模式
  • C#基础语法,如静态构造函数等。
  • 多线程编程。

 

三、数组中重复的数字

题目一:找出数组中重复的数字。在一个长度为n的数组里的所有数字都在0~n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是重复的数字2或者3。

测试用例:

  • 长度为n的数组里包含一个或多个重复的数字。
  • 数组中不包含重复的数字。
  • 无效输入测试用例,如输入空指针、长度为n的数组中包含0~n-1之外的数字。

时间复杂度和空间复杂度均为O(n)的解法:

int solve(int arr[], int len)
{
	int ans = -1;						// 没有重复的数字则返回-1 
	int *pArr = new int[len]();			// 对动态数组进行值初始化 
	for(int i = 0; i != len; ++i) 
	{
		++pArr[arr[i]];	
	}
	for(int i = 0; i != len; ++i) {
		if(pArr[i] > 1) {
			ans = i;
			break;
		}
	}
	delete []pArr;
	return ans;
}

补:书中的解法是时间复杂度为O(n),而空间复杂度为O(1)。

 

题目二:不修改数组找出重复的数字。在一个长度为n+1的数组里的所有数字都在1~n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为8的数组{2,3,5,4,3,2,6,7},那么对应的输出是重复的数字2或者3。

分析:这一题和上面的面试题类似,但题目明确要求不能修改输入的数组。我们可以像上题一样,创建一个长度为n+1的辅助数组,但该方案需要O(n)的辅助空间。我们要设法尝试避免使用O(n)的辅助空间。但正如下面这个算法所示,即使它的空间复杂度为O(1),但其时间复杂度却为O(nlogn),而且它不能保证找出所有重复的数字。故我们选取什么算法应取决于面试官提出的功能要求或性能要求。

思路:由题意可以确定数组中一定有重复的数字,故可以把从1~n的数字从中间的数字m分为两部分,前面一半为1~m,后面一半为m+1~n。如果1~m的数字的数目超过m,那么这一半的区间里一定包含重复的数字;否则,另一半m+1~n的区间里一定包含重复的数字。然后,继续把包含重复数字的区间一分为二,直到找到一个重复的数字。

 

 

四、在二维数组中的查找

题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

测试用例:

  • 二维数组中包含查找的数字。
  • 二维数组中没有查找的数字。
  • 特殊输入测试。

解法:

bool solve(int *matrix, int rows, int cols, int x)
{
	bool found = false;
	if(rows > 0 && cols > 0)
	{
		int row = 0, col = cols - 1;
		while(row < rows && col >= 0)
		{
			if(matrix[row * cols + col] == x)
			{
				found = true;
				break;
			}
			else if(matrix[row * cols + col] > x)
				--col;
			else
				++row;
		}
	}
	return found;
}

分析:若从二维数组(矩形)的中间选取一个数字来比较,那么如果该数字与目标值不等的话,接下来的查找将没有固定的范围。而如果从右上角或左下角开始进行比较,则会逐渐缩小查找范围,最终得到结果。所以,该题考察的是应聘者能否通过具体的例子来找出其中的规律。

 

五、替换空格

题目:请实现一个函数,把字符串中的每个空格替换成“%20”。例如,输入“We are happy!”,则输出“We%20are%20happy!”。

题意:假设面试官是让我们在原来的字符串上进行替换,并且保证输入的字符串后面有足够多的空余内存。

测试用例:

  • 输入的字符串中包含空格,空格可位于字符串的最前面、最后面或中间,甚至可以有连续多个空格。
  • 输入的字符串中没有空格。
  • 特殊输入测试,如字符串是一个空字符串,或字符串只有一个空格字符。

时间复杂度为O(n2)的解法:

从头到尾扫描字符串,每次碰到空格字符的时候进行替换。而由于是把1个字符替换成3个字符,所以我们必须要把空格后面所有的字符都后移2字节,否则就有两个字符被覆盖了。

假设字符串的长度为n,对每个空格字符,需要移动后面O(n)个字符,因此对于含有O(n)个空格字符的字符串而言,总的时间效率是O(n2)。

时间复杂度为O(n)的解法:

先遍历一次字符串,这样就能统计出字符串中空格的总数,并可以由此计算出替换之后的字符串的总长度。我们从字符串的后面开始复制和替换。

 

六、从尾到头打印链表

题目:输入一个链表的头节点,从尾到头反过来打印出每个节点的值。

测试用例:

  • 功能测试,如输入的链表有多个节点,或输入的链表只有一个节点。
  • 特殊输入测试,如输入的链表头节点指针为nullptr。

栈+循环的解法:

struct ListNode {
	int value;
	ListNode *next;
};

void print_list(ListNode *pHead)
{
	stack<int> sck;
	ListNode *pNode = pHead;
	while(pNode) {
		sck.push(pNode->value);
		pNode = pNode->next;
	}
	while(!sck.empty()) {
		cout << sck.top() << endl;
		sck.pop();
	}
}

栈+递归的解法:

struct ListNode {
	int value;
	ListNode *next;
};

void print_list_recursively(ListNode *pHead)
{
	if(pHead != nullptr) {
		if(pHead->next != nullptr) {
			print_list_recursively(pHead->next);
		}
		cout << pHead->value << cout;
	}
}

补充:既然想到了用栈来实现该函数,并且递归在本质上就是一个栈结构,于是很自然地又想到了用递归来实现。

注意:当链表非常长的时候,就会导致函数调用的层级很深,从而有可能导致函数调用栈溢出。

 

七、重建二叉树

题目:输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树并输出它的头节点。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如,输入前序遍历序列{1, 2, 4, 7, 3, 5, 6, 8}和中序遍历序列{4, 7, 2, 1, 5, 3, 8, 6},则重建如下图所示的二叉树并输出它的头节点。

测试用例:

  • 普通二叉树。
  • 特殊二叉树,如只有一个节点的二叉树、所有节点都没有右子节点的二叉树、所有节点都没有左子节点的二叉树。
  • 特殊输入测试,如二叉树的根节点指针为nullptr、输入的前序遍历序列和中序遍历序列不匹配。

考点:

  • 对二叉树的前序遍历和中序遍历的理解程度。
  • 分析复杂问题的能力,把构建二叉树的大问题分解成构建左、右子树的两个小问题,而小问题和大问题在本质上是一致的,故可以用递归的方式解决。
struct BinaryTreeNode {
	int m_nValue;
	BinaryTreeNode *m_pLeft;
	BinaryTreeNode *m_pRight;
};

BinaryTreeNode* Construct(int* preorder, int* inorder, int length)
{
    if(preorder == nullptr || inorder == nullptr || length <= 0)
        return nullptr;

    return ConstructCore(preorder, preorder + length - 1, inorder, inorder + length - 1);
}

BinaryTreeNode* ConstructCore(int* startPreorder, int* endPreorder, int* startInorder, int* endInorder)
{
    // 前序遍历序列的第一个数字是根结点的值
    int rootValue = startPreorder[0];
    BinaryTreeNode* root = new BinaryTreeNode();
    root->m_nValue = rootValue;
    root->m_pLeft = root->m_pRight = nullptr;

    if(startPreorder == endPreorder)
    {
        if(startInorder == endInorder && *startPreorder == *startInorder)
            return root;
        else
            throw std::exception("Invalid input.");
    }

    // 在中序遍历中找到根结点的值
    int* rootInorder = startInorder;
    while(rootInorder <= endInorder && *rootInorder != rootValue)
        ++ rootInorder;

    if(rootInorder == endInorder && *rootInorder != rootValue)
        throw std::exception("Invalid input.");

    int leftLength = rootInorder - startInorder;
    int* leftPreorderEnd = startPreorder + leftLength;
    if(leftLength > 0)
    {
        // 构建左子树
        root->m_pLeft = ConstructCore(startPreorder + 1, leftPreorderEnd, 
            startInorder, rootInorder - 1);
    }
    if(leftLength < endPreorder - startPreorder)
    {
        // 构建右子树
        root->m_pRight = ConstructCore(leftPreorderEnd + 1, endPreorder,
            rootInorder + 1, endInorder);
    }

    return root;
}

小结:其实面试不会让写这么一道题,但容易考到的是先序遍历的递归/循环实现方法,对于中序遍历、后序遍历也是如此。

 

八、二叉树的下一个节点

题目:给定一棵二叉树和其中的一个节点,如何找出中序遍历序列的下一个节点?树中的节点除了有两个分别指向左、右子节点的指针,还有一个指向父节点的指针。

分析:此题需要应聘者在掌握中序遍历的基础上,通过具体的例子来找出其中的规律,并由此设计出可行的算法。

解法:

BinaryTreeNode* find_next(BinaryTreeNode *pNode)
{
	if(pNode == nullptr)
		return nullptr;
	BinaryTreeNode *pNext = nullptr;
	if(pNode->m_pRight != nullptr) {
		BinaryTreeNode *pTemp = pNode->m_pRight;
		while(pTemp->m_pLeft != nullptr)
			pTemp = pTemp->m_pLeft;
		pNext = pTemp;
	}
	else if(pNode->m_pParent != nullptr) {
		BinaryTreeNode *pCurrent = pNode;
		BinaryTreeNode *pParent = pNode->m_pParent;
		while(pParent != nullptr && pCurrent == pParent->m_pRight) {
			pCurrent = pParent;
			pParent = pParent->m_pParent;
		}
		pNext = pParent;
	}
	return pNext;
}

  

九、用两个栈实现队列

题目:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数appendTail和deleteHead,分别完成在队列尾部插入节点和在队列头部删除节点的功能。

测试用例:

  • 往空的队列里添加、删除元素。
  • 往非空的队列里添加、删除元素。
  • 连续删除元素直至队列为空。

解法:

stack<int> sck1;
stack<int> sck2;

void appendTail(int val)
{
	sck1.push(val);
}

int deleteHead()
{
	if(sck2.empty()) {
		while(!sck1.empty()) {
			int temp = sck1.top();
			sck1.pop();
			sck2.push(temp);
		}
	}
	if(sck2.empty())
		throw new exception("queue is empty");
	int ret = sck2.top();
	sck2.pop();	
	return ret;
}

分析:这道题的意图是要求我们操作两个“后进先出”的栈sck1和sck2以实现一个“先进先出”的队列。当添加元素时,我们总是向栈sck1内插入;而删除元素时,则总是从栈sck2内删除。

拓展:用两个队列实现一个栈。分析思路见书p71。

 

十、斐波拉契数列

题目一:求斐波那契数列的第n项。写一个函数,输入n,求斐波那契(Fibonacci)数列的第n项。

递归解法:

int fibonacci(int n)
{
	if(n == 0)	return 0;
	if(n == 1)	return 1;
	return fibonacci(n-1) + fibonacci(n-2);
}

分析:许多教科书在讲解递归算法时,总是以此段代码作为示例,但该解法有很严重的效率问题。一句话阐明,该递归代码分解的两个子问题中存在大量重复的计算

循环解法:

int fibonacci(int n)
{
	if(n == 0)	return 0;
	if(n == 1)	return 1;
	int x1 = 0;
	int x2 = 1;
	int ans = 0;
	for(int i = 2; i <= n; ++i) {
		ans = x1 + x2;
		x1 = x2;
		x2 = ans;
	}
	return ans;
}

分析:上述递归代码之所以慢,是因为存在大量重复的计算,因此,我们只要想办法避免重复计算就行了。该解法是从下往上计算,首先根据f(0)和f(1)算出f(2),再根据f(1)和f(2)算出f(3)……以此类推就可以算出第n项了。

 

题目二:青蛙跳台阶问题。一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个n级的台阶总共有多少种跳法。

分析:把n级台阶时的跳法看成n的函数,记为f(n),这就不难看出该问题就是斐波那契数列了。

 

 

  

  

 

posted @ 2018-08-22 23:06  GGBeng  阅读(200)  评论(0编辑  收藏  举报