开发:C++开发面经
最近面试中遇到一些问题
最近面试中遇到一些问题
算法是编程的基础框架,就像是建房子的砖头,生产的原料,爸妈做饭的柴米油盐。没有良好的算法基础,哪里做得出好菜,生产出优质的产品,建造出结实的房子。
1 | void* operator new(std::size_t) throw(std::bad_alloc); |
关键在于声明的依存性替换定义的依存性。 如果使用object reference或者obj pointers可以完成任务,就不要使用objects。你可以只依靠一个类型声明式 就定义出指向该类型的references和pointers
根据一棵树的前序遍历与中序遍历构造二叉树。 > 注意:你可以假设树中没有重复的元素。 ### 例如, 给出 前序遍历 preorder = [3,9,20,15,7] 中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树:
3
/
9 20 /
15 7
前序遍历顺序是遍历根节点,左子树,右子树,而中序遍历则是左子树,根节点,右子树,因此这类题目的解题思路是根据前序遍历的第一个元素确定根节点,然后在中顺遍历中找到根节点的位置。在中序遍历的左侧是左子树,右侧是右子树。 如上面的例子,首先我们根据前序的第一个节点确定3是根节点,那么在中序遍历结果中找到3,那么中序遍历结果中左侧的序列【9】则是3为根节点的左子树的中序结果,而右侧的序列【15,20,7】则是右子树的中序结果。 确定了左右子树,继续在左子树的中序遍历结果中找到出现在先序遍历结果的元素,因为在先序遍历结果首先出现的一定是子树的根节点。如本题,左子树的中序遍历结果为【9】,只有一个元素,那么一定是9先出现在先序的结果中,因此左子树根节点为9。右子树的中序遍历结果为【15,20,7】,那么首先出现在先序遍历结果【3,9,20,15,7】的元素是20,那么20是右子树的根节点。 因为左子树根节点9在其子树对应的中序结果【9】中没有左侧和右侧的序列,那么9则是一个叶子节点。而右子树根节点20在其对应子树的中序结果【15,20,7】中存在左侧序列【15】和右侧序列【7】,那么【15】对应的则是以20为根节点的左子树的中序结果,而【7】则是以20为根节点的右子树的中序结果。循环递归上面的过程构造子树。 反应到程序中需要解决两个重要的问题: 1. 先序遍历结果的第一个元素(根节点)在中序遍历结果中的位置如何确定? 2. 左子树中序遍历子序列的根节点,即左子树的根节点如何确定?同样,右子树中序遍历子序列的根节点,即右子树的根节点如何确定?
代码 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if(preorder.size()==0) return NULL; //空树
TreeNode* root = new TreeNode(preorder[0]);
if(preorder.size()==1) return root; //只有一个节点
vector<int> leftIn,leftPre,rightIn,rightPre;
int location = 0;
while(inorder[location]!=root->val){
leftIn.push_back(inorder[location]);
location++;
}
for(int i=1;i<=location;i++) leftPre.push_back(preorder[i]);
for(int i=location+1;i<preorder.size();i++){
rightPre.push_back(preorder[i]);
rightIn.push_back(inorder[i]);
}
root->left = buildTree(leftPre, leftIn);
root->right = buildTree(rightPre, rightIn);
return root;
}
}
思路 二叉树或一般树的水平层次遍历,可以使用BFS(广度搜素)算法,使用队列 Queue标记每一层的结点元素; Queue:先进先出, 后进后出。可以保证每一层遍历时的结点顺序; BFS:类似于电影中的病毒传染,先感染靠近自己的,再由易感染层感染更外层; 该题二叉树中,先把根结点压入队列,当队列不为空时,移除队首结点,并判断该结点的左右子树中有无非空结点,若存在,则再次入队对应的左右子树结点……同一层的每个结点循环以上操作,直至队列为空,循环结束。
1 | #include <vector> |
1 | class Solution { |
时间复杂度:O(n) 空间复杂度:O(n)
1 | class Solution { |
vector<int>
1 | 1 |
这个问题可以通过广度有限搜索的方式实现,关键是要找到每一层最右边的那个节点。
1 | #include <vector> |
btree如果一共有\(n\)个节点,该算法的时间复杂度是\(O(n)\), 因为我们是遍历了一遍所有的节点。 空间复杂度: \[x(1 + \frac{1}{2} + \frac{1}{4} + ..) = x(\frac{1}{1- \frac{1}{2}}) = n \] \[x = \frac{n}{2}\] 所以最长的队列长度是\(\frac{n}{2}\),那么空间复杂度为\(O(n)\)
首先最简单的实现方法是使用两个deque来实现,这样每次deque保存当前的层,然后最后出队的就是尾部节点。但是这个是采用两个队列,空间上有一些浪费资源,需要二外的\(k/2\)的空间资源。
Given a string s and a non-empty string p, find all the start indices of p's anagrams in s.
Strings consists of lowercase English letters only and the length of both strings s and p will not be larger than 20,100.
The order of output does not matter.
Example 1:
Input: s: "cbaebabacd" p: "abc"
Output: [0, 6]
Explanation: The substring with start index = 0 is "cba", which is an anagram of "abc". The substring with start index = 6 is "bac", which is an anagram of "abc".
Example 2:
Input: s: "abab" p: "ab"
Output: [0, 1, 2]
Explanation: The substring with start index = 0 is "ab", which is an anagram of "ab". The substring with start index = 1 is "ba", which is an anagram of "ab". The substring with start index = 2 is "ab", which is an anagram of "ab".
用两个哈希表,分别记录p的字符个数,和s中前p字符串长度的字符个数,然后比较,如果两者相同,则将0加入结果res中,然后开始遍历s中剩余的字符,每次右边加入一个新的字符,然后去掉左边的一个旧的字符,每次再比较两个哈希表是否相同即可,参见代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Solution {
public:
vector<int> findAnagrams(string s, string p) {
if (s.empty()) return {};
vector<int> res, m1(256, 0), m2(256, 0);
for (int i = 0; i < p.size(); ++i) {
++m1[s[i]]; ++m2[p[i]];
}
if (m1 == m2) res.push_back(0);
for (int i = p.size(); i < s.size(); ++i) {
++m1[s[i]];
--m1[s[i - p.size()]];
if (m1 == m2) res.push_back(i - p.size() + 1);
}
return res;
}
};
首先统计字符串p的字符个数,然后用两个变量left和right表示滑动窗口的左右边界,用变量cnt表示字符串p中需要匹配的字符个数,然后开始循环, - Step1: 如果右边界的字符已经在哈希表中了,说明该字符在p中有出现,则cnt自减1,然后哈希表中该字符个数自减1,右边界自加1, - Step2:如果此时cnt减为0了,说明p中的字符都匹配上了,那么将此时左边界加入结果res中。 - Step3:如果此时right和left的差为p的长度,说明此时应该去掉最左边的一个字符,我们看如果该字符在哈希表中的个数大于等于0,说明该字符是p中的字符,因为上面Step1我们有让每个字符自减1,如果不是p中的字符,那么在哈希表中个数应该为0,自减1后就为-1,所以这样就知道该字符是否属于p,如果我们去掉了属于p的一个字符,cnt自增1. 参见代码如下:
1 | class Solution { |
https://www.cnblogs.com/grandyang/p/6014408.html
给定两个字符串A和B,长度分别为m和n,要求找出它们最长的公共子序列,并返回其长度。例如: A = "HelloWorld" B = "loop"
则A与B的最长公共子序列为 "loo",返回的长度为3。此处只给出动态规划的解法:定义子问题dp[i][j]为字符串A的第一个字符到第 i 个字符串和字符串B的第一个字符到第 j 个字符的最长公共子序列,如A为“app”,B为“apple”,dp[2][3]表示 “ap” 和 “app” 的最长公共字串。注意到代码中 dp 的大小为 (n + 1) x (m + 1) ,这多出来的一行和一列是第 0 行和第 0 列,初始化为 0,表示空字符串和另一字符串的子串的最长公共子序列,例如dp[0][3]表示 "" 和 “app” 的最长公共子串。
当我们要求dp[i][j],我们要先判断A的第i个元素B的第j个元素是否相同即判断A[i - 1]和 B[j -1]是否相同,如果相同它就是dp[i-1][j-1]+ 1,相当于在两个字符串都去掉一个字符时的最长公共子序列再加 1;否则最长公共子序列取dp[i][j - 1] 和dp[i - 1][j]中大者。所以整个问题的
1 | int findLCS(string A, int n, string B, int m) |
给定两个字符串A和B,长度分别为m和n,要求找出它们最长的公共子串,并返回其长度。例如: A = "HelloWorld" B = "loop" 则A与B的最长公共子串为 "lo",返回的长度为2。 我们可以看到子序列和子串的区别:子序列和子串都是字符集合的子集,但是子序列不一定连续,但是子串一定是连续的。
这里只给出动态规划的解法:定义dp[i][j]表示以A中第i个字符结尾的子串和B中第j个字符结尾的子串的的最大公共子串(公共子串实际上指的是这两个子串的所有部分)的长度(要注意这里和LCS的不同,LCS中的dp[i+1][j+1]一定是大于等于dp[i][j]的;但最长公共子串问题就不一定了,它的dp[i][j]表示的子串不一定是以A[0]开头B[0]开头的,但是一定是以A[i-1]、B[j-1]结尾的),同样地, dp 的大小也为 (n + 1) x (m + 1) ,这多出来的一行和一列是第 0 行和第 0 列,初始化为 0,表示空字符串和另一字符串的子串的最长公共子串。
当我们要求dp[i][j],我们要先判断A的第i个元素B的第j个元素是否相同即判断A[i - 1]和 B[j -1]是否相同,如果相同它就是dp[i - 1][j- 1] + 1,相当于在两个字符串都去掉一个字符时的最长公共子串再加 1;否则最长公共子串取0。
整个问题的初始状态为: dp[i][0]=0, dp[0][j]=0
相应的状态转移方程为: \[dp[i][j] = \begin{cases} 0 ,& {A[i - 1] != B[j - 1]} \\ dp[i - 1][j - 1] + 1 , & {A[i - 1] == B[j - 1]} \end{cases}\]
代码的实现如下:
1 | class LongestSubstring { |
该算法的时间复杂度为O(nm),空间复杂度为O(nm)。同样地,遍历下标也是从1开始的。不过关于最长公共子串问题,有几点需要注意下:
1.由于dp[i][j]不像LCS是个递增的数组,所以它在每次更新时需要同时更新最大值rs,且最后返回的结果是rs。而LCS中返回的直接就是dp[n][m]。 2.从代码上来看,两者的结构其实差不多,只不过状态转移方程有些小许的不同,分析过程也类似。
Given an unsorted array of integers, find the number of longest increasing subsequence.
Example 1:
Input: [1,3,5,4,7]
Output: 2
Explanation: The two longest increasing subsequence are [1, 3, 4, 7] and [1, 3, 5, 7].
Example 2:
Input: [2,2,2,2,2]
Output: 5
Explanation: The length of longest continuous increasing subsequence is 1, and there are 5 subsequences' length is 1, so output 5.
https://www.cnblogs.com/grandyang/p/7603903.html
示例 1: - 输入: [1,2,3,4,5,6,7] 和 k = 3 - 输出: [5,6,7,1,2,3,4] - 解释: - 向右旋转 1 步: [7,1,2,3,4,5,6] - 向右旋转 2 步: [6,7,1,2,3,4,5] - 向右旋转 3 步: [5,6,7,1,2,3,4]
示例 2: - 输入: [-1,-100,3,99] 和 k = 2 - 输出: [3,99,-1,-100] - 解释: - 向右旋转 1 步: [99,-1,-100,3] - 向右旋转 2 步: [3,99,-1,-100]
说明:
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
要求使用空间复杂度为 O(1) 的原地算法。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
void rotate(vector<int>& nums, int k) {
k %= nums.size();
vector<int> temp(k);
for(int i = 0; i < k; i++) temp[i] = nums[nums.size() - 1 - i];
for(int i = nums.size() - 1; i >= 0; i--) {
nums[i] = nums[i - k];
}
for(int i = 0; i < k; i++) {
nums[i] = temp[k - i - 1];
}
return;
}
};
类似翻转字符的方法,先把前n-k个数字翻转一下,再把后k个数字翻转一下,最后再把整个数组翻转一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Solution {
public void rotate(vector<int> &nums, int k) {
int count = k % nums.size();
int bounder= nums.size() - count;
reverse(nums,0,bounder-1);
reverse(nums,bounder, nums.size() - 1);
reverse(nums,0,nums.size() - 1);
}
private void reverse(vector<int> &arr,int st,int end){
while(st < end){
int temp = arr[st];
arr[st] = arr[end];
arr[end] = temp;
st++;
end--;
}
}
};
Given a string, sort it in decreasing order based on the frequency of characters.
Example 1:
Input: "tree"
Output: "eert"
Explanation: 'e' appears twice while 'r' and 't' both appear once. So 'e' must appear before both 'r' and 't'. Therefore "eetr" is also a valid answer.
Example 2:
Input: "cccaaa"
Output:"cccaaa"
Explanation: Both 'c' and 'a' appear three times, so "aaaccc" is also a valid answer. Note that "cacaca" is incorrect, as the same characters must be together.
Example 3:
Input: "Aabb"
Output: "bbAa"
Explanation: "bbaA" is also a valid answer, but "Aabb" is incorrect. Note that 'A' and 'a' are treated as two different characters.
这道题让我们给一个字符串按照字符出现的频率来排序,那么毫无疑问肯定要先统计出每个字符出现的个数,那么之后怎么做呢?我们可以利用优先队列的自动排序的特点,把个数和字符组成pair放到优先队列里排好序后,再取出来组成结果res即可,参见代码如下:
1 | class Solution { |
我们也可以使用STL自带的sort来做,关键就在于重写comparator,由于需要使用外部变量,记得中括号中放入&,然后我们将频率大的返回,注意一定还要处理频率相等的情况,要不然两个频率相等的字符可能穿插着出现在结果res中,这样是不对的。参见代码如下:
1 | class Solution { |
我们也可以不使用优先队列,而是建立一个字符串数组,因为某个字符的出现次数不可能超过s的长度,所以我们将每个字符根据其出现次数放入数组中的对应位置,那么最后我们只要从后往前遍历数组所有位置,将不为空的位置的字符串加入结果res中即可,参见代码如下:
1 | class Solution { |
编写一个高效的算法来判断\(m \times n\)矩阵中,是否存在一个目标值。该矩阵具有如下特性: - 每行中的整数从左到右按升序排列。 - 每行的第一个整数大于前一行的最后一个整数。 - 示例输入:
matrix = [ [1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 50]] target = 3
输出: true
1.找规律,首先此二维数组是有序的,我们可以从右上角开始查找,每次只需要左移或下移即可,也就是row++或col--; 2.初始化右上角数字下标的指针常量,如果target等于当前数则return true,如果大于右上角的数字,那么target肯定不在当前行,row++,省去了一行的比较,如果target小于右上角的数字,则target肯定不在当前列,那么col++即可。 3.完结。
根据二维数组数值特点,将其想象成为我们熟悉的一维数组求解。而这里二维转成一维的关键是一维数组的下标mid和二维数组下标[i][j]的换算关系:[i][j]=[mid/列数][mid%列数]。直接上代码,比较简介应该很容易看懂,就不再赘述了。
1 | class Solution { |
1 | char** mymalloc(const int m, const int n) { |
Hog特征结合分类算法广泛应用于图像识别中,尤其是行人检测中获得极大成功。
算法是编程的基础框架,就像是建房子的砖头,生产的原料,爸妈做饭的柴米油盐。没有良好的算法基础,哪里做得出好菜,生产出优质的产品,建造出结实的房子。
使用caffe框架进行实验过程中的一些心得
算法是编程的基础框架,就像是建房子的砖头,生产的原料,爸妈做饭的柴米油盐。没有良好的算法基础,哪里做得出好菜,生产出优质的产品,建造出结实的房子。
数组
数组可以直接通过下标进行访问,数组直接访问时没有开销的,但是插入和删除需要移动数组元素,开销比较大,因此插入和删除比较频繁地情况下不适合使用数组。数组中查找一个元素的时间复杂度是O(n),但是有序数组的情况下可以使用二分查找降到O(logn)
链表
链表的插入和删除时间是固定的时间,就是更改指针的时间,比数组的插入删除效率高。,但是访问链表元素的效率比较低,需要从链表的头部向后搜索,查找操作的时间是O(n),理论上链表的长度是不受限制的,但是时间使用过程中,链表受存储空间的限制,因此也是不能够无限增长的。
注意的是链表的头结点。因为在头结点之前进行插入或者删除头结点会导致头结点的指针失效。为了解决这个问题,我们设计了一个称谓哨兵节点或者哑节点的头节点,该节点没有数据域,只有指针域的特殊节点。这样我们就可以使用一致的方法处理空链表和非空链表;同时对链表进行插入删除不需要对数据元素的首届点和中间节点进行差异处理。
栈
具有后进先出的特点。常常用栈: * 算法转化为非递归实现 * 穷经搜索是存储当前状态 * 深度优先搜索
队列
队列是一种先进先出的特点的数据结构。此外尤其要注意优先队列的应用。
没有环路的图 > 二叉查找树
AVL树和红黑树
B树
B 树的节点大小为什么是4K?而不是采用更小的,例如二叉树一样一个节点有两个。
区间树:以区间为数据元素的红黑树
线段树:以区间为数据元素的二叉查找树
堆 - 最大堆:每个节点的值都大于其子树所有节点的值 - 最小堆:每个节点的值都小于其子树所有节点的值
一个循环有序数组(如:3, 4, 5, 6, 7, 8, 9, 0, 1, 2),不知道其最小值的位置,要查找任一数值的位置。要求算法时间复杂度为\(log_2(n)\)
1.将一个循环有序数组一分为二,一定得到一个有序数组和另一个循环有序数组
2.长度不超过2的循环有序数组其实就是有序数组。
注意:a1=an这种情况。 * 判断左边一半和右边一半哪一个是有序的。这里以增加型的举例,减少型的同理。如果a[mid] >= a[start],那么左边一定是有序的。因为如果左边是循环有序的,那么最大值点一定出现在左侧,且最大值点左侧的数恒大于最大值点右侧的数。这与a[mid] >= a[start]矛盾。反之同理。
确定了有序的一侧后,就要判断是不是在这一侧搜索了。这个判断非常简单,只要确定待搜索的数的值是否在有序数列的两个端点值之间即可。
最后通过循环,就可以类似二分法,找到待搜索的数的位置。
Caffe install in Ubuntu