链表专题
链表结构体定义:
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
1.链表中倒数第K个结点
倒数第k个结点,从1开始数,倒数第0个结点指针为NULL。
空间复杂度O(n),时间复杂度O(n)解法:
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if(!pListHead) return NULL;
if(!k) return NULL;
vector<ListNode*> r;
ListNode *tmp=pListHead;
while(tmp){
r.insert(r.begin(), tmp);
tmp=tmp->next;
}
if(k<=r.size()) return r[k-1];
else return NULL;
}
};
倒数第k个就是顺数第n-k+1个(从1开始计数),先遍历得到链表长度n,再找到第n-k+1个结点。
空间复杂度O(1),时间复杂度O(2n-k)解法:
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if(!pListHead || !k) return NULL;
int len=0; ListNode *p=pListHead;
while(p) {p=p->next; len++;}
p=pListHead;
if(len>=k){
for(int i=0;i<len-k;i++) p=p->next;
return p;
}
return NULL;
}
};
找第n-k+1个结点的时间复杂度O(n)解法:
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if(!pListHead || !k) return NULL;
ListNode *kth=NULL, *p=pListHead;
int idx=1;
while(p){
if(idx==k){
kth=pListHead; //屏蔽了前k次循环后,剩下每次循环从开头向后移动一个结点
}
else if(idx>k){
kth=kth->next;
}
idx++;
p=p->next;
}
return kth;
}
};
2.反转链表
输入一个链表,反转链表后,输出新链表的表头。
时O(n),空O(1):
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
ListNode *cur=pHead, *pre=NULL;
while(cur){
ListNode *tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
};
3.从尾到头打印链表
输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
要返回一个ArrayList,所以空间复杂度至少O(n),可以直接使用一个vector,遍历链表每次插入到vector起始位置。
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> v;
while(head){
v.insert(v.begin(), head->val);
head = head->next;
}
return v;
}
};
4.合并两个排序的链表
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
设置三个指针a,b,c,分别指向要合并的两个链表和合成的链表,每次将a,b指向的较小的值连接到c上,并将相应的指针后移。
保护原链表版本,空O(n):
class Solution {
public:
ListNode* Merge(ListNode* A, ListNode* B)
{
ListNode *chead = new ListNode(0); //空结点,头指针
ListNode *c = chead;
ListNode *a = A;
ListNode *b = B;
while(a&&b){
ListNode *cur;
if(a->val<=b->val){
cur = new ListNode(a->val);
a = a->next;
}
else{
cur = new ListNode(b->val);
b = b->next;
}
c->next = cur;
c = c->next;
}
if(a){
c->next = a;
}
if(b){
c->next = b;
}
return chead->next;
}
};
不保护原链表版本,空O(1):
class Solution {
public:
ListNode* Merge(ListNode* A, ListNode* B)
{
ListNode *chead = new ListNode(0);
ListNode *c = chead;
ListNode *a = A;
ListNode *b = B;
while(a&&b){
ListNode *cur;
if(a->val<=b->val){
cur = a;
a = a->next;
}
else{
cur = b;
b = b->next;
}
c->next = cur;
c = c->next;
}
if(a){
c->next = a;
}
if(b){
c->next = b;
}
return chead->next;
}
};
5.两个链表的第一个公共结点
输入两个链表,找出它们的第一个公共结点。
与第1题类似,同样可以从链表的长度入手,设长链表长度为L1,短链表长度为L2,可以让长链表指针先走L1-L2步,之后便可以同步走指针了,第一个地址相等的结点就是公共结点了。
时O(2*L1+L2)
class Solution {
public:
ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2) {
if(!pHead1 || !pHead2) return NULL;
ListNode *p1=pHead1, *p2=pHead2;
int cnt1=0, cnt2=0;
while(p1) {p1=p1->next; cnt1++;}
while(p2) {p2=p2->next; cnt2++;}
p1=pHead1, p2=pHead2;
if(cnt1<cnt2) {ListNode *tmp=p1; p1=p2; p2=tmp;} //让p1指向长链表
for(int i=0;i<abs(cnt1-cnt2);i++) p1=p1->next;
while(p1!=p2){
p1=p1->next;
p2=p2->next;
}
return p1;
}
};
一般碰上和长度相关的链表操作,都可以用一些骚操作降低一点时间复杂度,时O(L1+L2)解法:
// 复杂度低,代码又清爽,就是不容易想出来..
class Solution {
public:
ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2) {
if(pHead1 == NULL || pHead2 == NULL)return NULL;
ListNode *p1 = pHead1;
ListNode *p2 = pHead2;
while(p1!=p2){
p1 = p1->next;
p2 = p2->next;
if(p1 != p2){
if(p1 == NULL) p1 = pHead2;
if(p2 == NULL) p2 = pHead1;
}
}
return p1;
}
};
6.复杂链表的复制
每个结点都有3个域,分别是
label
数据域、random
指针域、next
指针域,要求完成对这种类型的链表的完全复制。
时O(n),空O(n):
/*struct RandomListNode {
int label;
struct RandomListNode *next, *random;
RandomListNode(int x) :
label(x), next(NULL), random(NULL) {
}
};*/
class Solution {
public:
map<RandomListNode*, RandomListNode*> mp;
RandomListNode* copy_(RandomListNode* a)
{
if(a==NULL) return NULL;
if(mp.find(a)==mp.end()){
RandomListNode *b = new RandomListNode(a->label);
mp[a]=b;
return b;
}
else return mp[a];
}
RandomListNode* Clone(RandomListNode* pHead)
{
RandomListNode *newH = NULL;
RandomListNode *pre = NULL;
for(RandomListNode *p1=pHead; p1!=NULL; p1=p1->next){
RandomListNode *p2 = copy_(p1);
RandomListNode *p2_random = copy_(p1->random);
p2->random = p2_random;
if(newH==NULL){
newH=p2;
pre=newH;
}
else{
pre->next = p2;
pre = p2;
}
}
return newH;
}
};
其他写法:
class Solution
{
public:
RandomListNode* Clone(RandomListNode* pHead)
{
if(pHead == nullptr)
{
return nullptr;
}
std::unordered_map<RandomListNode*, RandomListNode*> hash_map;
for (RandomListNode* p = pHead; p != nullptr; p = p->next)
{
hash_map[p] = new RandomListNode(p->label);
}
for (RandomListNode* p = pHead; p != nullptr; p = p->next)
{
hash_map[p]->next = hash_map[p->next];
hash_map[p]->random = hash_map[p->random];
}
return hash_map[pHead];
}
};
7.删除链表中重复的结点
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
错误代码:无法通过的样例{1,1,2,2,3,3},正确输出应为{},下面代码输出{2,2}
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead)
{
if(!pHead) return pHead;
ListNode *new_head = pHead;
if(new_head->next && new_head->val == new_head->next->val){
int pre_val = new_head->val;
while(new_head && new_head->val == pre_val){
new_head = new_head->next;
}
}
if(new_head==NULL) return new_head;
ListNode *before = new_head;
ListNode *cur = new_head->next;
while(cur){
if(cur->next && cur->next->val==cur->val){
int _val = cur->val;
while(cur && cur->val==_val)
cur = cur->next;
before->next = cur;
}
else{
before = cur;
cur = cur->next;
}
}
return new_head;
}
};
错误原因:单独拎出了一段重复代码来判断头结点,没有统一代码框架,导致一些细节问题出错。很多情况下统一的逻辑框架可以减少错误的发生。
正确方法:一般来说,当需要一段重复代码来判断头结点时,那么可以在原始头结点之前再加上一个空结点作为辅助,使用一个空的头结点作为before结点,可以统一逻辑框架,避免重复代码。时O(n),空O(1)。
ListNode* deleteDuplication(ListNode* pHead)
{
ListNode *new_head = new ListNode(0); // 使用空的头结点作为辅助
new_head->next = pHead;
ListNode *before = new_head; // 前一个留下来的结点
ListNode *cur = pHead;
while(cur){
if(cur->next && cur->next->val==cur->val){ // 提前预判
int pre_val = cur->val;
while(cur && cur->val==pre_val) // 循环里面有cur=cur->next,就要判断cur是否为空
cur = cur->next;
before->next = cur;
}
else{
before = cur;
cur = cur->next;
}
}
return new_head->next; // 返回空的头结点的下一个结点
}
栈与队列专题
1.用两个栈实现队列
class Solution
{
public:
void push(int node) {
in.push(node);
}
int pop() {
if(out.empty()){
while(!in.empty())
out.push(in.top()), in.pop();
}
int t=-1;
if(!out.empty()){
t = out.top();
out.pop();
}
return t;
}
private:
stack<int> in;
stack<int> out;
};
字符串专题
1.替换空格
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
笔试碰上能用python直接秒:
# -*- coding:utf-8 -*-
class Solution:
# s 源字符串
def replaceSpace(self, s):
# write code here
s = s.replace(" ", '%20')
return s
- 不知道为啥发生段错误的C++代码,时O(n),空O(n):
class Solution {
public:
void replaceSpace(char *str,int length) {
if(str==NULL || length<=0)
return ;
int num =0;
for (int i = 0; i < length; i++){
if (str[i] == ' ')
num++;
}
char *p = (char *)malloc(strlen(str)+1+num*2);
int j=0;
for(int i=0;i<length;i++) p[j++]=str[i];
j=0;
for(int i=0;i<length;i++){
if(p[i]!=' ')
str[j++]=p[i];
else
str[j++]='%', str[j++]='2', str[j++]='0';
}
str[j]='\0';
free(p);
}
};
某个小伙伴的时O(n)、空O(1)的做法:
双指针,倒着来
class Solution {
public:
void replaceSpace(char* str, int length) {
if(str == NULL || length <0)
return;
int num =0; //空格数目
for (int i = 0; i < length; i++){
if (str[i] == ' ')
num++;
}
char* p1 = &str[length -1];//原字符串指针
char* p2 = &str[length -1 +num*2];//新字符串指针
for (int i = length-1; i >= 0; i--) {
if (str[i] == ' ') {
*p2-- = '0';
*p2-- = '2';
*p2-- = '%';
p1--;
}else{
*p2-- = *p1--;
}
}
}
};
二叉树专题
1.重建二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
递归解法
- 时间复杂度
T ( n ) = 2 T ( n / 2 ) + n T(n)=2T(n/2)+n T(n)=2T(n/2)+n,通过迭代可知 T ( n ) = n l o g n + n T(n)=nlogn+n T(n)=nlogn+n,故时 O ( n l o g n ) O(nlogn) O(nlogn) - 空间复杂度
上限是 S = 2 h ∗ h + 2 h − 1 ∗ ( h − 1 ) + . . . + 2 0 ∗ 0 S=2^h*h+2^{h-1}*(h-1)+...+2^0*0 S=2h∗h+2h−1∗(h−1)+...+20∗0,等差乘等比,先乘 q q q再相减,得 S = O ( n l o g n ) S=O(nlogn) S=O(nlogn),故空 O ( n l o g n ) O(nlogn) O(nlogn)可以使用左右边界指针,配合全局vector,或配合常引用vector(传参),使得空间复杂度降到O(1),但可读性没下面的代码好。
/**
* 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<int> pre,vector<int> in)
{
if(pre.empty()) return NULL;
if(pre.size()==1) return new TreeNode(pre[0]);
TreeNode *root = new TreeNode(pre[0]);
int ind=-1;
for(int i=0;i<in.size();i++){
if(in[i]==root->val){
ind = i;
break;
}
}
vector<int> lpre, lin;
for(int i=0;i<ind;i++) lin.push_back(in[i]);
for(int i=0;i<ind;i++) lpre.push_back(pre[1+i]);
root->left = reConstructBinaryTree(lpre, lin);
vector<int> rpre, rin;
for(int i=ind+1;i<in.size();i++) rin.push_back(in[i]);
for(int i=ind+1;i<in.size();i++) rpre.push_back(pre[i]);
root->right = reConstructBinaryTree(rpre, rin);
return root;
}
};
2.二叉搜索树的第k个结点
给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
二叉搜索树的中序遍历即为升序序列,于是可以使用中序遍历,遍历到第k个结点后输出。时O(n),空O(1):
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
int cnt=0;
int flag=0; //是否已找到
TreeNode* ans=NULL;
void inOrder(TreeNode *root, int k)
{
if(!root) return;
if(flag) return;
inOrder(root->left, k);
cnt++;
if(cnt==k){
ans=root;
flag=1;
}
inOrder(root->right, k);
}
TreeNode* KthNode(TreeNode* pRoot, int k)
{
inOrder(pRoot, k);
return ans;
}
};
3.平衡二叉树
输入一棵二叉树,判断该二叉树是否是平衡二叉树。
在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树。
递归计算深度,在计算左右子树的深度的同时判断是否平衡。时O(n),空O(n)(递归深度最大为n)。
class Solution {
public:
bool balance=true;
int treeDepth(TreeNode *root)
{
if(!root) return 0;
if(!balance) return -1;
int ld=treeDepth(root->left);
int rd=treeDepth(root->right);
if(abs(ld-rd)>1)
balance=false;
return max(ld, rd)+1;
}
bool IsBalanced_Solution(TreeNode* pRoot) {
if(!pRoot) return true;
treeDepth(pRoot);
return balance;
}
};
动态规划(递推)
1.跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
状态转移方程:T(n)=T(n-1)+T(n-2),即斐波那契数列,T(n)代表n级台阶的跳法数量。
时O(n),空O(1):
class Solution {
public:
int jumpFloor(int n) {
if(n<=0) return 0;
if(n<=2) return n;
int a=1, b=2, c;
for(int i=3;i<=n;i++){
c=a+b;
a=b; b=c;
}
return c;
}
};
2.变态跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
T(n)=T(n-1)+T(n-2)+…+T(1)+T(0),其中T(1)=T(0)=1,整理后得T(n)=2^(n-1),n>0。
时、空O(1):
class Solution {
public:
int jumpFloorII(int n) {
if(n==0) return 0;
return 1<<(n-1);
}
};
3.斐波那契数列
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。
n<=39
直接递归会超时,n=39时,计算次数达到63245986,即6e7,在超时的边缘:
int Fibonacci(int n) {
if(n==0) return 0;
if(n==1) return 1;
return Fibonacci(n-1)+Fibonacci(n-2);
}
空间换时间,空O(n),时O(n):
class Solution {
public:
int ans[50];
void getAns(){
ans[0]=0; ans[1]=1;
for(int i=2;i<=39;i++)
ans[i]=ans[i-1]+ans[i-2];
}
int Fibonacci(int n) {
getAns();
return ans[n];
}
};
4.矩形覆盖
我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
考虑第1块瓷砖(最左边)的放法,第1块瓷砖要么竖着放,要么横着放:若竖着放则后面是一个f(n-1)的子问题,若横着放则第2块瓷砖必须跟着横放在下面,于是接下来是一个f(n-2)的子问题,对第1块瓷砖放法的分类是不重不漏的,所以f(n)=f(n-1)+f(n-2),即斐波那契递推。
时O(n)
class Solution {
public:
int rectCover(int n) {
int a=0,b=1,c=0;
for(int i=0;i<n;i++){
c=a+b;
a=b, b=c;
}
return c;
}
};
模拟
1.旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
时O(n),空O(1):
class Solution {
public:
int minNumberInRotateArray(vector<int> rotateArray) {
if(rotateArray.size()==0) return 0;
int ans=rotateArray[0];
for(int i=1;i<rotateArray.size();i++){
int cur=rotateArray[i];
int pre=rotateArray[i-1];
if(cur<pre){
ans=cur;
break;
}
}
return ans;
}
};
2.二维数组中的查找
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
考虑矩阵左下角的元素,往上递减,往右递增,于是每次当target比左下角的元素小的时候就往上移动一格(当前行可以不用考虑了),target比左下角的元素大的时候就往右移动一格(当前列可以不用考虑了)。
时O(n+m)
class Solution {
public:
bool Find(int target, vector<vector<int> > a) {
int n=a.size();
int m=a[0].size();
int i=n-1, j=0;
while(i>=0&&i<n && j>=0&&j<m && a[i][j]!=target){
if(a[i][j]<target) j++;
else if(a[i][j]>target) i--;
}
if(i>=0&&i<n && j>=0&&j<m) return true;
else return false;
}
};
3.二进制中1的个数
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
n为正数时正常,负数时超时:
int NumberOf1(int n) {
int cnt=0;
while(n){
if(n&1) cnt++;
n=n>>1;
}
return cnt;
}
原因是负数左移1位,最高位自动补1而不是0,于是n为负数时上面的程序会陷入死循环。
测试:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define N 1005
#define mod 1000000007
#define INF 0x3f3f3f3f
const double eps=1e-8;
const double pi=acos(-1.0);
//assumes little endian
void printBits(size_t const size, void const * const ptr)
{
unsigned char *b = (unsigned char*) ptr;
unsigned char byte;
int i, j;
for (i=size-1;i>=0;i--)
{
for (j=7;j>=0;j--)
{
byte = (b[i] >> j) & 1;
printf("%u", byte);
}
}
puts("");
}
int main()
{
int i=-1;
printBits(sizeof(i), &i);
int j=i>>1;
printBits(sizeof(j), &j);
return 0;
}
输出:
11111111111111111111111111111111
11111111111111111111111111111111
解决:既然给的接口是int,那么可以直接暴力数这32位的二进制数中有多少个1。
int NumberOf1(int n){
int cnt=0;
for(int i=0;i<32;i++){ // 每次只检查第i位
if((n>>i)&1) cnt++;
}
return cnt;
}
其他解法:
int NumberOf1(int n) {
int count = 0;
while(n!= 0){
count++;
n = n & (n - 1); // n-1会把n最右边的1后面的0全部变成1,而原来最右边的1会变成0
// 即从最右边的1开始按位取反,于是n&(n-1)每次把最右边的1变为0
}
return count;
}
4.数值的整数次方
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
保证base和exponent不同时为0
注意int可能为负数,O(n)解法:
class Solution {
public:
double Power(double base, int exponent) {
int cnt=abs(exponent);
double ans=1;
for(int i=0;i<cnt;i++)
ans*=base;
if(exponent>=0)
return ans;
else
return 1/ans;
}
};
快速幂,O(log n):
class Solution {
public:
double Power(double a, int b){
int flag=(b>=0?1:0);
b=abs(b);
double ans=1;
while(b){
if(b&1) ans*=a;
b=b>>1;
a=a*a; // 将b看成二进制位,比如2^5,看成2^(101),
// b二进制位的相邻位的权重是平方关系
}
return flag?ans:1/ans;
}
};
5.调整数组顺序使奇数位于偶数前面
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
开辟两个vector,遍历原数组,是奇数就放v1,是偶数就放v2,再合并回去,时O(n),空O(n):
class Solution {
public:
void reOrderArray(vector<int> &a) {
vector<int> v1,v2;
for(int i=0;i<a.size();i++){
if(a[i]&1) v1.push_back(a[i]);
else v2.push_back(a[i]);
}
for(int i=0;i<v1.size();i++) a[i]=v1[i];
for(int i=v1.size();i<a.size();i++) a[i]=v2[i-v1.size()];
}
};
利用冒泡排序的思想,时O(n^2),空O(1):
class Solution {
public:
void reOrderArray(vector<int> &a) {
for(int i=a.size()-1;i>=0;i--){ // 每次确保第i个位置的元素是偶数(如果偶数没有排完的话)
int flag=0; // 是否互换过元素
for(int j=0;j<i;j++){
if((a[j]&1)==0 && (a[j+1]&1)==1){ // &优先级比==低,需括号!!
flag=1;
int tmp=a[j];
a[j]=a[j+1];
a[j+1]=tmp;
}
}
if(!flag) break; // 没有互换过元素则说明偶数已经排完,此时可以直接退出循环
}
}
};