3、数组中重复的数字
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
方法一:用STL提供的排序方法进行排序,然后对新数组进行相邻元素比较,如果相同,则输出该数字;因为重复的数字任意选出任意一个即可,即使打乱顺序也没有关系。空间复杂度为O(1),时间复杂度为O(nlogn + n)
方法二:建立哈希表,以数组中的数字作为key,以是否出现过作为value。空间复杂度O(n),时间复杂度O(n)。
方法三:找自己的位置。本题的特点在于数组长度为n,而所有数字的范围都在0-n-1之间,因此每个数字都能找到自己的“坑”使得numbers[i]=i,从头遍历数组,依次将第i个坑填入正确的数字,比如第0位填入0,第1位填入1…在填坑如果发现自己的坑已经被占了,就表示找到了重复的数。(表述还不够清楚)
4、二维数组中的查找
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
方法一:暴力检索,时间复杂度O(m*n),空间复杂度O(1)
方法二:从右上角开始搜索,如果(i,j)比target大,那么第j列都不可能存在target,此时进行j=j-1,如果(i,j)比target小,那么第i行都不可能存在target,此时进行i=i+1,如果(i,j)=target,说明找到了数字,如果到了左下角还没有找到,说明不存在。注意这个方法,只能从右上角或者左下角开始搜索,不能从右下角或左下角开始,到了某个节点会卡住,无法确认target在哪个方位,只能再暴力搜索。
5、替换空格
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
方法一:从头暴力操作,时间复杂度O(nn)
方法二:从后面往前面替换。先统计空格数量,设置两个指针p1,p2,p1指向原字符串的尾端,p2指向结果字符串的尾端,若p1是字母,则*p2=*p1,若为空格,则在p2处插入“%20”,时间复杂度O(n)。
注意这题给出的length是字符串长度,已经预留了额外的位置将空格改为%20,但也不保证够位置,所以要先统计原字符串的真实长度,从第一位到’\0’,统计空格数量,以此来计算最终结果的长度,看看length是否大于这个长度,如果不是则需要直接返回false。
6、从尾到头打印链表
输入一个链表,按链表值从尾到头的顺序返回一个ArrayList
方法一:用vector的insert()方法,从头部插入值,最后返回这个vector,时间复杂度O(n^2),空间复杂度O(1),每次从头部插入新的值,后面的元素都要向后移动。
方法二:用vector的push_back方法,最后用.reverse()方法。reverse()并不是vector的方法,而是alogrithm里的方法,不清楚复杂度,时间消耗从5ms下降到4ms,内存消耗稍有提升
方法三:用stack。时间复杂度O(2n),空间复杂度O(n)
方法四:递归。递归可以实现单向链表的逆序输出
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> result;
if(head!=nullptr)
reverseList(head,result);
return result;
}
void reverseList(ListNode* head, vector<int> &result){
//这个地方一定要加&,否则result的修改只在函数内有效
if(head->next!=nullptr)
reverseList(head->next,result);
result.push_back(head->val);
return;
}
};
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* reConstructNode(vector<int> pre, int prebegin, int preend,
vector<int> vin, int vinbegin, int vinend){
if(prebegin>=preend || vinbegin >= vinend) //等号必须要加
return nullptr;
TreeNode* root = new TreeNode(pre[prebegin]);
int pivot; //pivot是b上的绝对偏移,不是相对偏移,所以下面有一个-vinbegin的操作
for(pivot=vinbegin;pivot<vinend;pivot++){
if(vin[pivot]==pre[prebegin])
break;
}
root->left = reConstructNode(pre,prebegin+1,prebegin+pivot-vinbegin+1,vin, vinbegin,pivot);
root->right = reConstructNode(pre,prebegin+pivot-vinbegin+1,preend,vin,pivot+1,vinend);
return root;
}
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
TreeNode* result = reConstructNode(pre,0,pre.size(),vin,0,vin.size());
return result;
}
8、二叉树的下一个结点
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
方法:分类讨论:1、如果该节点有右子树,则去找右子树的最左节点,2、若没有右子树,但属于父节点的左节点,则返回其父节点;3、若没有右子树,而且属于父节点的右节点,则需要一直向上遍历,直到找到一个节点,该节点是其父节点的左节点,返回该节点的父节点,但是不一定能找到,如果找不到,也说明给定的节点就是中序遍历的最后一个节点了。
注意在向上遍历时,要一直判断是否存在父节点。
9、用两个栈实现队列
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
方法:一个栈用来做入队列,负责push操作,另一个栈用来做出队列,负责pop操作;push时直接push到stack1,pop时先判断stack2是否为空,若为空,则将stack1的元素按照出栈的顺序pop到stack2里面,再pop出stack2的顶部元素;若非空,直接pop出stack2的顶部元素。
10、斐波那契数列
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39
方法一:递归。非常非常耗时,在牛客上超时;
方法二:用循环,从下往上算。用f0+f1算出f2,f1+f2算出f3,…,一直算出fn,时间复杂度O(n)
方法三:转为矩阵运算,时间复杂度O(logn),书上有,略
同性质问题:
1、青蛙跳台阶问题:一只青蛙一次可以跳上一级台阶,也可以跳上两级台阶,求该青蛙跳上一个n级台阶总共有多少种跳法。
2、用2x1的小矩形去覆盖2x8的矩形,总共有多少种方法。
24、反转链表
输入一个链表,反转链表后,输出新链表的表头。
方法一:用堆栈,先入后岀。注意反转之后,新的链表尾的next要置为nullptr。空间复杂度为O(N),时间复杂度为O(2N)
方法二:其实用不着堆栈也能实现O(N)的时间复杂度,需要用指向连续节点的三个指针p1,p2,p3,将p2->next保存在p3,然后将p2->next置为p1,即实现了相邻两个节点的反转,同时不会丢失断裂处下一个节点的位置,然后将p1,p2后移,如果p2为nullptr了,说明已经是链表尾了,此时链表尾正是p1,返回p1即可。同样的,一开始头节点即新的链表尾的next要置为nullptr
25、合并两个排序链表
方法一:新建一个链表及对应指针pNode,以及两个指针p1,p2,一个在链表A中,一个在链表B中。根据p1,p2指针指向的节点的数值大小,为新链表插入新节点pNode-> = new ListNode(p1->val)或者new ListNode(p2->val),然后p1或p2相应地后移。如果p1为空,那么就不需要再考虑链表A,接下来把链表B的节点一一赋值到新链表中,p2为空亦如是,如果都为空,则完成链表的合并。
这种方法思路简单,但是操作很繁杂,一开始要先确定头节点,并保存头节点的地址用作返回值,在合并过程中也要一直根据p1和p2是否为空采取不同的操作,此外无论两个链表是什么顺序,时间复杂度一定是O(m+n),对每个节点都一定会进行操作。空间复杂度为O(m+n),因为是完全新建了一个链表,节点也都是新的。
方法二:无需重建所有节点,只需要改变节点的指向顺序。先找到p1,p2较小的节点,作为头节点赋值给pNode,同时保存在newHead中,p1,p2相应后移,然后进入循环,判断p1,p2哪个较小,将pNode指向较小的那个节点,然后pNode=pNode->next,p1,p2也相应地后移。直接p1或p2中出现了空指针,指有一条链表已经到达了结尾,退出循环。在循环外部,将pNode指向还未遍历的链表的当前节点上(p1或者p2)。该方法空间复杂度为O(1),时间复杂度最差的时候才为O(m+n)
方法三:递归的方法其实与方法二没有本质上的不同。递归函数中每一步的操作就是根据p1,p2的大小,将pNode设置为较小的那个,然后pNode->next通过递归函数继续搜索,只是此时的输入参数是经过后移一位操作的p1和p2,函数结束时返回pNode。递归的中止条件是某个指针到达了结尾,这种情况的操作其实和一开始判断两个链表是否有一个为空时是一模一样的。
问题拓展:合并k个排序链表
26、树的子结构
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
方法:需要两个递归函数,分别对应两个步骤。步骤1:找起始点,即在A中找到一个节点的数值与B的根节点相同,若找到了该节点,如进入步骤2。该步骤可以用递归实现,如果A当前节点与B的根节点不同,则去左节点继续找,如还是找不到,就去右节点找,还是找不到就返回false。步骤2:从当前A的起始点开始,逐步判断A和B的对应节点是否相同,直到B遍历完成(nullptr),同样可以用递归实现,判断当前节点是否相同,如果是则继续往左右遍历,返回左节点和右节点结果的并,如果不同则返回false。
class Solution {
public:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
{
bool result=false;
//步骤1的关键点在于逐步判断,当前节点相同就开始用步骤2的方法检查是否真的是子结构,
//如果检查发现不对,即result=false,也不要立刻return false,因为还有其他情况,
//则A进入左节点,递归调用当前函数,判断起始点是否相等,如果还不对,还要再进入右节点
//对左右节点的递归可以合并为result = HasSubtree(pRoot1->left,pRoot2) || HasSubtree(pRoot1->right,pRoot2);
if(pRoot1==nullptr || pRoot2==nullptr)
return false;
if(pRoot1->val==pRoot2->val){
result = HasSubtreeCore(pRoot1,pRoot2);
}
if(!result)
result = HasSubtree(pRoot1->left,pRoot2);
if(!result)
result = HasSubtree(pRoot1->right,pRoot2);
return result;
}
bool HasSubtreeCore(TreeNode* pRoot1,TreeNode*pRoot2){
if(pRoot1==nullptr || pRoot2==nullptr){
if(pRoot1==nullptr && pRoot2!=nullptr)
return false;
else
return true;
}
if(pRoot1->val==pRoot2->val){
return HasSubtreeCore(pRoot1->right,pRoot2->right)
&& HasSubtreeCore(pRoot1->left,pRoot2->left);
}
else
return false;
}
};
注意一个误区,B是A的子结构并不要求从某一个节点开始,A底下的所有节点和B相同,只需要部分相同。下面这种情况是true
8 8
/ \ /\
8 7 9 2
/\
9 2
/\
4 7
27、二叉树的镜像
操作给定的二叉树,将其变换为源二叉树的镜像。
二叉树的镜像定义:源二叉树
8
/ \
6 10
/ \ / \
5 7 9 11
镜像二叉树
8
/ \
10 6
/ \ / \
11 9 7 5
方法:递归地交换左右子树的地址即可,遇到nullptr直接返回。注意:交换地址!交换val的话很麻烦。
28、对称的二叉树
请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
方法:定义一种新的前序遍历,根节点->右节点->左节点,如果树是对称的,那么这种遍历方式的输出遍历序列和原来的前序遍历的应该是相同,方法就是用两个指针分别取遍历,一个用前序遍历的方式,一个用变种前序遍历的方式,每一步都要求输出相同,若不同则不对称。但这种方式只有在树的结构是对称的情况下才管用,因此还要加一个判断,就是进行的比较的两个节点是否其中有一个是空而另一个非空,这种情况就是结构不对称,直接false。
34、复杂链表的复制
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
struct RandomListNode {
int label;
struct RandomListNode *next, *random;
RandomListNode(int x) :
label(x), next(NULL), random(NULL) {
}
};
方法一:
1、按照pNext的方向先构建出链表的主线,用哈希表保存原链表中每个节点的地址以及对应在第几个位置,用数组保存按顺序新链表中每个节点的地址。第二次遍历原始链表,对于每一个就链表中的random指向的节点,在哈希表中找到它是在第几个位置,回到数组中去获取对应的新链表中该位置的节点地址,将整个地址赋值给新链表的random。
更新:其实不需要一个哈希表一个数组,只需要一个哈希表就够了,哈希表存储的每一个节点的旧地址和新地址。
2、把新建立的节点插在对应的旧节点之后,这样在构建random的指向时,旧节点的random指向节点p1,则p1的下一个节点就是对应的新节点的random所指向的节点。在构建完整个链表之后。再进行拆分。