《剑指Offer》各面试题总结

目录
  • 前言
  • 面试题4 二维数组的查找
    • 一维二分法
    • 二维查找法
  • 面试题5:替换空格
    • 伪新建数组法
    • 从后往前法
  • 面试题6:从尾到头打印链表
  • 面试题7:重建二叉树
    • 题解
  • 面试题8:二叉树的下一个节点
    • 题解
  • 面试题9:用两个栈实现队列
    • 题解
  • 面试题10:斐波那契数列
    • 矩阵快速幂
  • 面试题11:旋转数组的最小数字
    • 二分查找
  • 面试题12:矩阵中的路径
    • 思路
  • 面试题13:机器人的运动范围
    • 思路
  • 面试题14:剪绳子
    • 贪心
    • 动态规划
  • 面试题15:二进制中1的个数
    • 思路
  • 面试题16:数值的整数次方
    • 思路
  • 面试题17:打印从1到最大的n位数
    • 思路
  • 面试题18:删除链表的节点
    • 题目一
      • 思路
    • 题目二
      • 思路
  • 面试题19:正则表达式匹配
    • 思路
  • 面试题20:表示数值的字符串
    • 思路
  • 面试题21:调整数组顺序使奇数位于偶数前面
    • 思路
  • 面试题22:链表中倒数第k个节点
    • 常规解法
    • 递归法
  • 面试题23:链表中环的入口节点
    • 书上题解有关快指针追上慢指针的证明
    • set标记
  • 面试题24:反转链表
    • 思路
  • 面试题25:合并两个排序的链表
    • 思路
  • 面试题26:树的子结构
    • 思路
  • 面试题27:二叉树的镜像
    • 思路
  • 面试题28:对称的二叉树
    • 思路
  • 面试题29:顺时针打印矩阵
    • 思路
  • 面试题30:包含min函数的栈
    • 思路
  • 面试题31:栈的压入、弹出序列
    • 思路
  • 面试题32:从上到下打印二叉树
    • 思路
  • 面试题33:二叉搜索树的后序遍历序列
    • 思路
  • 面试题34:二叉树中和为某一值的路径
    • 思路
  • 面试题35:复杂链表的复制
    • 思路
  • 面试题36:二叉搜索树与双向链表
    • 递归
  • 面试题37:序列化二叉树
    • 思路
  • 面试题38:字符串的排列
    • 思路
  • 面试题39:数组中出现次数超过一半的数字
    • 思路
  • 面试题40:最小的k个数
    • 基于快排的Partition函数
    • 基于优先队列(堆)
  • 面试题41:数据流中的中位数
    • 两个优先队列(堆)
  • 面试题42:连续子数组的最大和
    • 贪心
  • <--------To be continued

前言

找工作必刷书,每次学习其他内容前先刷一两题热热身,相对于ACM来说简单了很多,就当复习下学过的知识,顺便划划水,疫情期间还是太闲的。
题目在牛客网上都有,不过牛客网似乎没有非法输入样例,例如空指针作为测试数据啥的,为节省时间,所以一切从简,无视非法输入数据,给出的代码均在牛客网上AC就算了。
牛客网地址:https://www.nowcoder.com/ta/coding-interviews?page=1

面试题4 二维数组的查找

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

一维二分法

显然,每行或每列都是顺序排序,所以可以遍历每行,然后对每列进行二分查找,反之亦可。
时间复杂度O(nlogn),空间复杂度O(1)

二维查找法

这是书上的方法,只要将右上角的数字作为重点即可。
显然,对于右上角的数字X来说,其左边的必然小于X,其下面的必然大于X。
那么,当所需查找的数字小于X时,显然可以排除掉X所在的列,因为该列的数字大小均 >=X,反之则排出行即可。
时间复杂度O(n),空间复杂度O(1)

class Solution {
public:
    bool Find(int target, vector > array) {
        int max_row = array.size();
        int row = 0;
        int column = array[0].size()-1;

        while(row < max_row && column >= 0){
            if (target == array[row][column]) return true;
            else if (target < array[row][column]) column--;
            else row++;
        }

        return false;
    }
};

面试题5:替换空格

题目:请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。要求只能在原来的数组中替换,保证输入后的字符串有足够多的空余内存。

伪新建数组法

虽然题目要求了不能在原来数组替换,但是原来数组内存足够多啊,完全可以把原来数组的后面当作一个新数组即可,替换完后再将原来数组的起始地址再+原来数组的长度+1,这方法有点取巧,虽然符合题目要求,这里就懒得敲代码了。

从后往前法

先找出所有空格数,显然替换后的数组长度 = 旧数组长度 + 空个数*2,然后从后往前依次替换即可。

class Solution {
public:
	void replaceSpace(char *str,int length) {
		int num = 0;//空格数
		for(int i = 0;str[i];i++)
			num += (str[i] == ' ');
		int index_old = length;
		int index_new = length+num*2;

		while(index_old >= 0){
			if(str[index_old] != ' ') str[index_new--] = str[index_old--];
			else{
				str[index_new--] = '0';
				str[index_new--] = '2';
				str[index_new--] = '%';
				index_old--;
			}
		}

		return ;
	}
};

面试题6:从尾到头打印链表

简单递归即可,略。

面试题7:重建二叉树

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

题解

较为简单,仅给出代码

/**
 * Definition for binary tree
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution
{
public:
    TreeNode* reConstructBinaryTree(vector pre,vector vin) {
        if(pre.empty()) return nullptr;
        TreeNode *root = new TreeNode(pre[0]);
        auto it = find(vin.begin(),vin.end(),pre[0]);
        root->left = reConstructBinaryTree(
            vector(pre.begin()+1,pre.begin()+1+(it-vin.begin())),vector(vin.begin(),it));
        root->right = reConstructBinaryTree(
            vector(pre.begin()+1+(it-vin.begin()),pre.end()),vector(it+1,vin.end()));
        return root;
    }
};

面试题8:二叉树的下一个节点

题目:给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

题解

较为简单,两种情况:
1、若有右节点,则找出右节点的最左节点
2、若无右节点,则循环找出父节点,当一个父节点的左节点等于子节点时,返回父节点

/*
struct TreeLinkNode {
    int val;
    struct TreeLinkNode *left;
    struct TreeLinkNode *right;
    struct TreeLinkNode *next;
    TreeLinkNode(int x) :val(x), left(NULL), right(NULL), next(NULL) {
        
    }
};
*/
class Solution {
public:
    TreeLinkNode* GetNext(TreeLinkNode* pNode){
        if (pNode->right != nullptr){
            pNode = pNode->right;
            while(pNode->left != nullptr)
                pNode = pNode->left;
            return pNode;
        }
        while(true){
            TreeLinkNode *father = pNode->next;
            if(father == nullptr||father->left == pNode) return father;
            pNode = father;
        }
    }
};

面试题9:用两个栈实现队列

题目:用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

题解

较为简单,栈是先进后出,队列是先进先出,栈和队列正好相反,两个栈之间可以实现元素的反序。
举个例子,第一个栈依次放入{1,2,3,4},将第一个栈依次排除放入第二个栈中,第二个栈则变成了{4,3,2,1},此时pop出第二个栈便是队列的顺序。
注意:当第二个栈为空时才能进行此操作。

class Solution
{
public:
    void push(int node) {
        stack1.push(node);
    }

    int pop() {//懒得考虑队列为空的情况
        if(stack2.empty()){
            while(!stack1.empty()){
                stack2.push(stack1.top());
                stack1.pop();
            }
        }
        int ans = stack2.top();
        stack2.pop();
        return ans;
    }

private:
    stack stack1;
    stack stack2;
};

面试题10:斐波那契数列

题目:大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)

矩阵快速幂

递归效率太低,时间复杂度应该是\(O(2^n)\),如果使用记忆化搜索或循环的话,可以降到\(O(n)\),不过这两个都是常规操作,使用矩阵快速幂的话可以达到\(O(logn)\)
详见博文:https://www.cnblogs.com/MMMMMMMW/p/12300262.html

面试题11:旋转数组的最小数字

题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

二分查找

书上写的是递增,牛客的是非递减,所以需要注意的是可能会有相同元素。
对于牛客的非递减而言,就会出现{1,2,1,1,1,1,1}和{1,1,1,1,1,2,1}的情况,显然难以二分,这里就有坑了,只能一格一格移动,所以即使用二分,最坏情况下还是\(O(n)\),而书上的递增数组则可以\(O(logn)\),直接跳过这题了。

面试题12:矩阵中的路径

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如\(\begin{bmatrix} a & b & c &e \\ s & f & c & s \\ a & d & e& e\end{bmatrix}\)矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

思路

dfs和bfs都行吧,水题跳过。

面试题13:机器人的运动范围

地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?

思路

dfs或bfs,水题跳过

面试题14:剪绳子

给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],...,k[m]。请问k[0]xk[1]x...xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

贪心

书上题解很详细,略,只贴出代码

class Solution {
public:
    int cutRope(int number) {
        return number <= 3?number-1:((int)pow(3,number/3-(number%3==1))*(number%3?(number%3==1?4:2):1));
    }
};

动态规划

如式:\(f(n)=max[f(i)*f(n-i),f(n)]\),然而本题dp效果不如贪心,故略。

面试题15:二进制中1的个数

输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

思路

基础做法自然是先转化成二进制,再求解,但鉴于负数的存在(负数用二进制表示时,最高位表示负数,其他位表示正数,其值为和),可以考虑下其他解决方法,将一个数n最后一个1置0的方法为\(n &= n-1\),很好理解,一个数减1后,最后一个1变为0,后面的数变为1,再&原来的数,这部分就全变成0了。

int  NumberOf1(int n) {
    int count = 0;
    while(n) count++, n&=n-1;
    return count;
}

面试题16:数值的整数次方

给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
保证base和exponent不同时为0

思路

考虑各种特殊情况即可,可使用快速幂,略。

面试题17:打印从1到最大的n位数

这题牛客居然没有,没题目复制,看书吧。

思路

显然需要用到大数,java就舒服了,自带大数,C/C++就需要自己用字符串模拟,但并不算难,ACM准大一新生赛必出题,话说int达到1e9,long long则1e18,正常范围内都几乎不可能打印完的,代码略。

面试题18:删除链表的节点

题目一

给定单向链表的头指针和一个节点指针,O(1)内删除该节点。

思路

这题目简直在逗我,交换数据后删除下一个节点,虽说达到了功能,但准确来讲还是删除了下一个节点而不是该节点,不过正常操作下单向链表O(1)删除指定节点是不存在的,因为找不到前面的节点。

题目二

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

思路

思路不难,就是链表和指针有点恶心,还需要考虑各种特殊情况,比较麻烦。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* deleteDuplication(ListNode* pHead){
        ListNode *NewHead = new ListNode(0);//头部可能重复,因此头指针可能会删除,需自定义一个伪头指针
        ListNode *Now = pHead;
        ListNode *Pre = NewHead;
        while(Now != nullptr){
            bool need_delete = false;
            while(Now->next != nullptr && Now->val == Now->next->val){
                need_delete = true;
                Now = Now->next;
            }
            ListNode *pNext = Now->next;
            if(!need_delete){
                Pre->next = Now;
                Pre = Now;
                Pre->next = nullptr;
            }
            Now = pNext;
        }
        return NewHead->next;
    }
};

面试题19:正则表达式匹配

请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配

思路

简单模拟,模拟题思路都比较简单,就是考虑各种乱七八糟的情况,所以比较麻烦,需要考虑全,漏一个都不行,所以写起来比较恶心。

class Solution {
public:
    bool match(char* str, char* pattern){
        if (str == nullptr || pattern == nullptr)
            return false;
        if (*str == '\0' && *pattern == '\0')
            return true;
        if (*pattern == '\0' )
            return false;
        if (*(pattern+1) == '*' )
            return match(str,pattern+2) || ((((*pattern == '.' && *str != '\0') || *str == *pattern) &&(match(str+1,pattern))));
        if ((*pattern == '.' && *str != '\0') || *str == *pattern )
            return match(str+1,pattern+1);
        return false;
    }
};

面试题20:表示数值的字符串

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。

思路

模拟水题,需考虑各种情况,写起来恶心但思路很水,跳过

面试题21:调整数组顺序使奇数位于偶数前面

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

思路

两个指针,当前面指针指向偶数,后面指针指向奇数时,交换两个值即可,水题略,可通过函数指针实现其他功能。

面试题22:链表中倒数第k个节点

输入一个链表,输出该链表中倒数第k个结点。

常规解法

题解思路挺水的,需要遍历两遍,当然也可以用两个指针遍历,指针相距k-1后同时遍历,然而事实上这种解法遍历次数和前者一样,都是需要一个指针遍历到终点,一个指针遍历到答案点,本质上并无区别,水题略。

递归法

跳过两题了,还是得敲敲代码,然而遍历两遍的解法太常规了,这时候我觉得,如果要求链表的每个节点只能经过一次呢?也就是真正意义上遍历一遍,此时便可以使用递归法,遍历到终点时返回终点指针和计数1,此后递归返回时计数+1,这时候相当于从终点开始遍历了。

/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    pair FindKthToTail_pair(ListNode* pListHead,unsigned int k){
        if (pListHead->next == nullptr) return make_pair(pListHead,1);
        pair pre = FindKthToTail_pair(pListHead->next,k);
        if(pre.second == k) return pre;
        return make_pair(pListHead,pre.second+1);
    }
    ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
        if(pListHead == nullptr || k == 0)
            return nullptr;
        pair ans = FindKthToTail_pair(pListHead,k);
        return ans.second == k?ans.first: nullptr; //不等于说明整个链表长度不足
    }
};

面试题23:链表中环的入口节点

给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

书上题解有关快指针追上慢指针的证明

随便写写,并不规范
证明追上时必定在环上
显然,当快指针追上慢指针时,快指针显然已经循环过了,那么快指针必然在环上。
证明快指针必然会与慢指针在同一位置
快指针行走2t,慢指针行走t,慢指针进入环内后,开始时两者相距m,环的大小为n,相遇时快指针循环了x圈,慢指针循环了y圈,则必有\(2a + m = x*n\)\(a = y*n\),相减并整理得\(x - y = (a+m)/n\),其中m,n均固定,a每次加1,所以\((a+m)/n\)必然会出现整数的情况,即上式必然有解。

不过其实不用这么麻烦,考虑各种情况即可。

  • 慢指针在快指针前一格
    • 慢先走一格,快再走两格,相遇
    • 快先走两各,慢再走一个,相遇
  • 慢指针在快指针前两格
    • 慢先走一格,快再走两格,就变成上面的情况【慢指针在快指针前一格】
    • 快先走两个,直接相遇

所以,快指针一定能和慢指针相遇。

set标记

一个写起来轻松但并非高效的做法,使用set记录下经过的节点,只需要遍历一遍即可,不过使用set的时间复杂度为\(O(logn)\),故总时间复杂度为\(O(logn*n)\),再加上set所需的空间复杂度,效果肯定没题解好,但敲起来轻松点,书上的题解就懒得敲了。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* EntryNodeOfLoop(ListNode* pHead){
        set set_node;
        while(pHead != nullptr){
            if(set_node.count(pHead)) return pHead;
            set_node.insert(pHead);
            pHead = pHead->next;
        }
        return nullptr;
    }
};

面试题24:反转链表

输入一个链表,反转链表后,输出新链表的表头。

思路

略,仅提供代码

/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* ReverseList(ListNode* pHead) {
        ListNode* pPre = nullptr;

        while(pHead != nullptr){
            ListNode* temp = pHead->next;
            pHead->next = pPre;
            pPre = pHead;
            pHead = temp;
        }

        return pPre;
    }
};

面试题25:合并两个排序的链表

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

思路

/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2){
        if(pHead1 == nullptr || pHead2 == nullptr) return pHead1 == nullptr?pHead2:pHead1;
        ListNode* pNewHead = (pHead1->val < pHead2->val)?pHead1:pHead2;
        pNewHead->next = (pHead1->val < pHead2->val)? Merge(pHead1->next,pHead2):Merge(pHead1,pHead2->next);
        return pNewHead;
    }
};

面试题26:树的子结构

输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)。

思路

递归水题,仅给出代码

class Solution {
public:
    bool IsSubtree(TreeNode* pRoot1, TreeNode* pRoot2){
        if (pRoot2 == nullptr) return true;
        if (pRoot1 == nullptr) return false;
        return  abs(pRoot1->val - pRoot2->val)< 1e-6
                &&IsSubtree(pRoot1->left,pRoot2->left)
                &&IsSubtree(pRoot1->right,pRoot2->right);
    }
    bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2){
        return (pRoot1 == nullptr || pRoot2 == nullptr)?false
                :(IsSubtree(pRoot1,pRoot2) || HasSubtree(pRoot1->left,pRoot2) || HasSubtree(pRoot1->right,pRoot2));
    }
};

面试题27:二叉树的镜像

操作给定的二叉树,将其变换为源二叉树的镜像。

思路

递归,每次交换左右节点,水题略

面试题28:对称的二叉树

请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。

思路

中左右和中右左同时遍历。

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};
*/
class Solution {
public:
    bool isSymmetrical(TreeNode* pNode1,TreeNode* pNode2){
        return  (pNode1 == nullptr && pNode2 == nullptr)?true:
                (pNode1 != nullptr && pNode2 != nullptr
                &&pNode1->val == pNode2->val
                &&isSymmetrical(pNode1->left,pNode2->right)
                &&isSymmetrical(pNode2->right,pNode1->left));
    }
    bool isSymmetrical(TreeNode* pRoot){
        return isSymmetrical(pRoot,pRoot);
    }
};

面试题29:顺时针打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.

思路

找规律,例如题目给出的例子,其位置变化为y+3 x+3 y-3 x-2 y+2 x+1 y-1 ,规律很明显,代码能省就省,但面试千万别这么写

class Solution {
public:
    vector printMatrix(vector > matrix) {
        vector ans;
        if (matrix.empty() || matrix[0].empty()) return ans;
        int matrix_num_x = matrix.size(),matrix_num_y = matrix[0].size();
        int sum = matrix_num_x*matrix_num_y,check = 1,x = 0,y = matrix_num_y-1;
        for(int i = 0;i < matrix_num_y;ans.push_back(matrix[0][i++])); 
        while(ans.size() < sum ){
            for(int i = --matrix_num_x;i;i--,ans.push_back(matrix[x+= check][y]));
            check *= -1;
            for(int i = --matrix_num_y;i;i--,ans.push_back(matrix[x][y+= check]));
        }
        return ans;
    }
};

面试题30:包含min函数的栈

定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。
注意:保证测试中不会当栈为空的时候,对栈调用pop()或者min()或者top()方法。

思路

再定义一个辅助栈,其栈顶为最小值,每次压入新值时与辅助栈的栈顶比较,若大于等于,则在辅助栈继续压入栈顶的值,若小于,则在辅助栈压入新值,代码略。

面试题31:栈的压入、弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)

思路

按顺序压入栈,当栈顶等于弹出序列首个元素时,弹出栈顶并删除弹出序列的首个元素,当压入结束后栈中仍有元素时,则返回false。

class Solution {
public:
    bool IsPopOrder(vector pushV,vector popV) {
        stack st;
        int point_push = 0,point_pop = 0;
        while(point_push < pushV.size()){
            st.push(pushV[point_push++]);
            while(!st.empty()&&popV[point_pop] == st.top()){
                point_pop++;
                st.pop();
            }
        }
        return st.empty();
    }
};

面试题32:从上到下打印二叉树

从上往下打印出二叉树的每个节点,同层节点从左至右打印。

思路

队列先进先出,题目二可以使用pair存储该节点第几层,题目三则是按层数奇偶判断即可,二三代码略。

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    vector PrintFromTopToBottom(TreeNode* root) {
        vector ans;
        queue que_print;
        if (root != nullptr) que_print.push(root);
        while(!que_print.empty()){
            TreeNode* front_que = que_print.front();
            que_print.pop();
            ans.push_back(front_que->val);
            if(front_que->left != nullptr) que_print.push(front_que->left);
            if(front_que->right != nullptr) que_print.push(front_que->right);
        }
        return ans;
    }
};

面试题33:二叉搜索树的后序遍历序列

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。

思路

二叉搜索树,值的大小左<中<右,后续遍历顺序左右中,显然最后一个为根,前面连续小于根的元素均为左子树部分,其他的为右子树,当右子树部分有小于根的情况,则不是二叉搜索树,返回false,否则对左右子树继续递归判断。

class Solution {
public:
    bool VerifySquenceOfBST(vector sequence) {
        int len = sequence.size(),i = 0,j = 0;
        if (len == 0 || len == 1) return len;//数组只剩一个节点时,可生成二叉搜索树
        while(sequence[i] < sequence[len-1]) j=++i;
        while(j < len-1) if (sequence[j++] (sequence.begin(),sequence.begin()+i)))
        &&(!(len-i-1)?true:VerifySquenceOfBST(vector(sequence.begin()+i,sequence.end()-1)));
    }
};

面试题34:二叉树中和为某一值的路径

输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前)。

思路

bfs或递归均可,递归为fuc(左右子树,k=值-当前节点值),当到叶节点时判断当前值与k是否相等即可,代码略。
需要注意的是,题目要求数组长度大的数组靠前,而bfs是从短到长,因此每次加入新数组时可以从头加,而递归则无序,需要重新排序。

面试题35:复杂链表的复制

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

思路

难点在于另个一特殊指针,它指向的是原来链表的位置,但复制的话就需要指向新链表的位置,方法很多,例如两重循环\(O(n^2)\),用map将特殊节点对应\(O(logn*n)/O(n)\),map也可用unorded_map,即哈希来使得时间复杂度\(O(n)\),但这些的空间复杂度都较高,于是最好的肯定就是时间\(O(n)\),空间\(O(1)\)的做法了,做法看书即可,还算明了,就是写代码的时候有些大坑,例如特殊指针可能为空,可能从空指针取值而导致崩溃,此外还有牛客上提交的代码千万不能改变原来的链表结构,因此拆分时需要还原旧链表,不然会显示输出结果为空,我之前拆分的时候就只管拆出新链表,而不管旧链表,坑了好久百度后才发现必须将旧链表还原。

/*
struct RandomListNode {
    int label;
    struct RandomListNode *next, *random;
    RandomListNode(int x) :
            label(x), next(NULL), random(NULL) {
    }
};
*/
class Solution {
public:
RandomListNode* Clone(RandomListNode* pHead){
        RandomListNode* pNewHead = nullptr;//无需判断头指针为空情况,因为运行过程中已经判断了
        for(RandomListNode *pNewTemp,*pOldTemp = pHead;pOldTemp!= nullptr;pOldTemp = pNewTemp->next){
            pNewTemp = new RandomListNode(pOldTemp->label);
            if (pNewHead == nullptr) pNewHead = pNewTemp;
            pNewTemp->next = pOldTemp->next;
            pOldTemp->next = pNewTemp;
        }
        for(RandomListNode* pTemp = pHead;pTemp != nullptr;pTemp = pTemp->next->next)
            pTemp->next->random = (pTemp->random== nullptr? nullptr:pTemp->random->next);//注意:特殊节点可能为空
        for(RandomListNode* pTemp = pHead;pTemp != nullptr;pTemp = pTemp->next) {
            RandomListNode* pNext = pTemp->next->next;
            if (pNext != nullptr) pTemp->next->next = pNext->next;//空指针取值会崩溃
            pTemp->next = pNext;
        }
        return pNewHead;
    }
};

面试题36:二叉搜索树与双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

递归

对于一个节点来说,其左边连接左子树的最右(大)节点,其右边连接右子树的最左(小)节点,如此便是排序的双向链表。

TreeNode* Convert_(TreeNode* pRootOfTree,bool is_left){
    if (pRootOfTree == nullptr) return nullptr;
    if (pRootOfTree->left != nullptr) {
        pRootOfTree->left = Convert_(pRootOfTree->left,true);
        pRootOfTree->left->right = pRootOfTree;
    }
    if (pRootOfTree->right != nullptr) {
        pRootOfTree->right = Convert_(pRootOfTree->right,false);
        pRootOfTree->right->left = pRootOfTree;
    }
    if (is_left) return pRootOfTree->right == nullptr?pRootOfTree:pRootOfTree->right;
    else return pRootOfTree->left == nullptr?pRootOfTree:pRootOfTree->left;
}
TreeNode* Convert(TreeNode* pRootOfTree){
    TreeNode* ans = Convert_(pRootOfTree,true);
    while(ans != nullptr && ans->left != nullptr)
        ans = ans->left;
    return ans;
}

面试题37:序列化二叉树

请实现两个函数,分别用来序列化和反序列化二叉树。

思路

前中后序均可,将空指针用特殊字符替代,代码略。

面试题38:字符串的排列

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。(可能有字符重复)

思路

我记得当时准大一,在暑假的ACM新生赛时遇到过这题,花了挺长时间找规律解决的,没用递归(初学C语言),当时的方法也忘了,不过印象挺深的。
递归,需要注意的是,可能有字符重复,所以需要去重,例如"aaa",就只能输出一个结果,不过,其实C/C++自带实现排列的函数,但是面试的时候肯定不能用的,不过机试的时候只要AC就行,如果不查代码的话倒是可以用用。

//正常面试写法
class Solution {
public:
    void Permutation(vector &ans,string &str,int begin){
        if (!str[begin]) ans.push_back(str);
        for(int i = begin;str[i];i++){
            swap(str[begin],str[i]);
            Permutation(ans,str,begin+1);
            swap(str[begin],str[i]);
        }
    }
    vector Permutation(string &str) {
        vector ans;
        if (str.empty()) return ans;
        Permutation(ans,str,0);
        sort(ans.begin(),ans.end());
        ans.erase(unique(ans.begin(),ans.end()),ans.end());
        return ans;
    }
};
//机试写法
class Solution {
public:
    vector Permutation(string &str) {
        vector ans;
        if (str.empty()) return ans;
        sort(str.begin(),str.end());
        do{
            ans.push_back(str);
        }while(next_permutation(str.begin(),str.end()));
        return ans;
    }
};

面试题39:数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

思路

剑指题解二,仅提供代码

class Solution {
public:
    int MoreThanHalfNum_Solution(vector numbers) {
        if (numbers.empty()) return 0;
        int cent = 1,pre = numbers[0],times = 0;
        for(int i = 1;i < numbers.size();i++)
            if (numbers[i] != numbers[i-1]){
                if (cent == 1) pre = numbers[i];
                else cent--;
            }
            else cent++;
        for(int i = 0;i < numbers.size();i++)
            if (pre == numbers[i]) times++;
        return times*2 > numbers.size()?pre:0;
    }
};

面试题40:最小的k个数

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。

基于快排的Partition函数

准确来说,该时间复杂度不应该是题解所说的\(O(n)\)

  • 最佳情况下是选择中位数作为基准数,这时候就能将数组一分为二,直接舍弃一半,遍历次数为\(S = n+n/2+n/4...\),时间复杂度倒是可以算作\(O(n)\)
  • 最差情况下是每次都选择了值最小或最大的元素作为基准值,那每次就只能舍弃一个值,遍历次数为\(S = n+(n-1)+(n-2)...\),所以时间复杂度为\(O(n^2)\)
  • 综合来说,时间复杂度介于\(O(n)\)\(O(n^2)\)之间
class Solution {
public:
    int partition(vector &input,int left,int right){
        int key = rand()%(right-left+1)+left;
        swap(input[key],input[left]);
        key = left;
        while(left < right){
            //如果两个where交换位置呢?
            //最后可能出现input[left] > input[key]&&left > key的情况
            //即最后交换后左侧会有一个值大于基准值
            while(left < right && input[right] >= input[key]) right--;
            while(left < right && input[left] <= input[key]) left++;
            if (left < right) swap(input[left],input[right]);
        }
        swap(input[left],input[key]);
        return left;
    }
    vector GetLeastNumbers_Solution(vector input, int k) {
            if (input.empty() ||  k > input.size() || --k < 0) return vector(input.begin(),input.begin());
            int left = 0,right = input.size();//范围[left,right)
            while(left < right){
                    int mid = partition(input,left,right-1);
                    if (mid == k) return vector(input.begin(),input.begin()+k+1);
                    else if (mid < k ) left = mid +1;
                    else right = mid;
            }
    }
};

基于优先队列(堆)

当数据量大,且不允许改变数据范围时,使用堆,时间复杂度\(O(nlogk)\)

class Solution {
public:
    vector GetLeastNumbers_Solution(vector input, int k) {
    		vector ans;
    		priority_queue que;
            if (input.empty() ||  k > input.size() || k <= 0) return ans;
            for(auto it = input.begin();it != input.end();it++)
                if (que.size() < k) que.push(*it);
                else if (que.top() > *it){
                    que.pop();
                    que.push(*it);
                }
           while(!que.empty()){
            	ans.push_back(que.top());
            	que.pop();
            }
            return ans;
    }
};

面试题41:数据流中的中位数

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

两个优先队列(堆)

左侧用最大堆,右侧用最小堆,显然中位数就是两个堆顶之一或两个堆顶的平均值

class Solution {
public:
    priority_queue max_que;
    priority_queue,greater > min_que;
    void Insert(int num){
        int sum = max_que.size()+min_que.size();
        if (sum&1) min_que.push(num);
        else max_que.push(num);
        if (!min_que.empty()&&min_que.top() < max_que.top()){//当右侧最小值小于左侧最大值时交换
            min_que.push(max_que.top());
            max_que.push(min_que.top());
            min_que.pop();
            max_que.pop();
        }
    }
    double GetMedian(){ 
        return ((max_que.size()+min_que.size())&1)?max_que.top():(max_que.top()+min_que.top())/2.0;
    }
};

面试题42:连续子数组的最大和

HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

贪心

思路略,仅提供代码

int FindGreatestSumOfSubArray(vector array) {
	if (array.empty()) return 0;
	int ans = array[0];
	int Max = array[0];
	int pre = 0;
	for(auto each:array){
		pre += each;
		Max = max(each,Max);
		ans = max(pre,ans);
		pre = max(pre,0);
	}
	return Max < 0?Max:ans;
}

<--------To be continued

你可能感兴趣的:(《剑指Offer》各面试题总结)