• 《剑指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),这就不难看出该问题就是斐波那契数列了。

      

      

  • 相关阅读:
    牛影传说【线段树+BFS序运用】
    动态规划 :传纸条
    CQYZ-OJ P1377 危险的组合
    使用 git 管理你的配置文件
    Exponential Distribution
    初尝 C++ 类设计
    Android刷机的一般步骤
    重装 Linux 记录
    Linux 折腾记录 (非正式)
    最大熵对应的概率分布
  • 原文地址:https://www.cnblogs.com/xzxl/p/9521032.html
Copyright © 2020-2023  润新知