LeetCode是一个程序员刷算法题的软件
《剑指offer》是通行全球的程序员经典面试秘籍
(简单)
1.找出数组中重复的数字
在一个长度为n的数组nums里的所有数字都在0~n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
例:
输入:
【2,3,1,0,2,5,3】
输出:2或3
解:
(1)基础正常解法就是利用两个for循环遍历数组,找到相同的元素就返回
class Solution
{
public:
int findNumber(vector<int>v)
{
for (int i = 0; i < v.size() - 1; i++)
{
for (int j = i + 1; j < v.size() - 1; j++)
{
if (v[i] == v[j])
{
return v[i];
}
}
}
}
};
但时间复杂度太大,需要更高效
(2)利用原地变换的方法来解决
即若数组下标与其值不对应(a[0]=2),则将以该值为下标的元素与该元素互换(a[2]=3,换完后a[0]=3,a[2]=2),若继续不对应则继续换,直到a[0]=0才往下走,经过交换后各个下标尽可能的会对应其值,此时若出现重复的值去找其对应下标就会发现值一样,即重复数字
class Solution
{
public:
int findNumber(vector<int>v)
{
int i = 0;
while (i < v.size() - 1)
{
if (v[i] != i)
{
swap(v[i], v[v[i]]);//交换值
}
else if(v[i]==v[v[i]])
{
return v[i];//若相等则重复
}
else
{
i++;
}
}
}
};
总结:若遇到查找重复元素的问题,可试着让元素与其对应下标或标签移动,此时就比较容易发现重复元素
(中等)
2. 二维数组中的查找
在一个n*m的二维数组中,每一行都按照从左到右非递减的顺序排序,每一列都按照从上到下非递减的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组是否含有该整数
例:
现有矩阵如下:
【
【1 , 4 , 7 , 11 , 15 】,
【2 , 5 , 8 , 12 , 19 】,
【3 , 6 , 9 , 16 , 22 】,
【10, 13 , 14 , 17 , 24 】,
【18, 21 , 23 , 26 , 30 】
】
查找元素4,返回TRUE
查找元素99,返回FALSE
解:
若直接遍历整个数组肯定达不到高效的要求,所以需要观察矩阵,发现其从左到右增大,从上到下增大,所以就类似一颗二叉树,15为其根节点,第一行左边的都为其左孩子,比他小,15这一列的都为其右孩子,比他大,所以可以利用这种性质,让查找元素与根节点比较,就像查找二叉树的元素一样
即若查找元素比15小则删除所在的列,不再需要考虑这一列(不再考虑右孩子),若查找元素比15大则删除所在行,不再需要考虑这一行(不再考虑左孩子),若相等则找到了
int findDate(vector<vector<int>>v, int date)
{
//i为行,j为列
for (int i = 0, j = 4; i<v.size() - 1, j>-1;)
{
if (date == v[i][j])
{
return TRUE;
}
if (date < v[i][j])
{
j--;//删除列
}
else
{
i++;//删除行,即逻辑上不再访问
}
}
return FALSE;
}
总结:发现其规律,大多数题都与数据结构挂钩,可根据特点与特性进行联想
左小右大为二叉树,先进先出为队列,先进后出为栈,一条逻辑链接为链表
(简单)
3.请实现一个函数,把字符串s中的每个空格替换成"%100"
例:
输入:s = "we are happy"
输出:"we%100are%100happy"
很明显,将空格替换后字符串长度变长了,所以需要计算新的字符串长度,而这与空格数量相关,然后要进行替换则必须移动元素,否则空间不够替换
若从前往后替换,则需要移动大量的元素(一旦字符串长),所以很明显我们需要从后往前替换(例题字符串比较短容易忽略这个问题),我们赋予字符串新的长度后,后边是有足够刚好的空间来给我们进行元素的移动的,从后往前移还可以让元素不被覆盖
string replacement(string s)
{
int x = 0;//标记空格数量
for (int i = 0; i < s.size() - 1; i++)
{
if (s[i] == ' ')
{
x++;
}
}
//%100比空格多占3个位,所以新的位数为空格数*3
int len = s.size();
s.resize(len + 3 * x);
//从后往前替换,利用两个指针的方法(不一定得是真的指针)
for (int i = len - 1, int j = s.size() - 1; i < j; i--, j--)
{
if (s[i] != ' ')
{
s[j] = s[i];//若不是空格则将该元素移动到后方,腾出位置
}
else
{
s[j - 3] = '%';
s[j - 2] = '1';
s[j - 1] = '0';
s[j] = '0';
j -= 3;//若遇到空格则替换为%100,记得类指针j需要后移3位
}
}
return s;
}
(简单)
4.从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)
例:
输入:head=[1 , 2 , 3 ]
输出:[2 , 3 , 1 ]
可利用栈的先进后出的特点进行存储再输出
struct ListNode
{
int val;
ListNode* next;
};
class Solution
{
public:
vector<int>reversePrint(ListNode* head)
{
stack<int>s;
ListNode* p = head;
//遍历单链表
while (p != NULL)
{
s.push(p->val);
p = p->next;
}
//用数组返回结果
vector<int>v;
while (s.empty() != NULL)
{
v.push_back(s.top());//返回栈顶元素
s.pop();//出栈
}
return v;
}
};
(中等)
5.重建二叉树
输入某二叉树的先序遍历和中序遍历的结果,请构建该二叉树并返回其根节点
假设输入的先序遍历和中序遍历的结果中都不含重复的数字
例:
1.
输入:preorder先序 = [ 3 , 9 , 20 , 15 , 7 ]
inorder中序 = [9 , 3 , 15 , 20 , 7 ]
输出:[ 3 , 9 , 20 , null , null , 15 , 7 ]
2.
输入:preorder = [ -1 ] , inorder = [ -1 ]
输出:[ -1 ]
class TreeNode
{
public:
TreeNode* left;
TreeNode* right;
int date;
};
class TreeNodeRoot
{
public:
TreeNode* root;
};
class Solution
{
private:
map<int, int>index;//定义一个哈希表来定位中序遍历中的根节点,以求快速找到
public:
//构造哈希表
//哈希表的键值用中序数组的值来表示
//哈希表的值用中序数组的值的下标来表示
//如此便可以根据键值(根节点)来快速查找下标
TreeNode* Index(vector<int>& preorder, vector<int>& inorder)
{
int n = preorder.size();
for (int i = 0; i < n; i++)
{
//将下标与值一一对应
index[inorder[i]] = i;
}
}
//参数分别为先序遍历数组,中序遍历数组,先序遍历左节点的下标,先序遍历右节点的下标,中序遍历左右节点的下标
TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right)
{
if (preorder_left > preorder_right)
{
return nullptr;//若先序左节点的下标大于右节点的下标,则表明其左孩子个数为0,返回空树
}
//在先序遍历中,第一个节点就是根节点
int preorder_root = preorder_left;//将下标赋给根节点
//在中序遍历中,根据哈希表查找根节点位置,定位根节点
int inorder_root = index[preorder[preorder_root]];
//把根节点建立出来
TreeNode* root = new TreeNode(preorder[preorder_root]);
//得出其左孩子的数量
int size_left = inorder_root - inorder_left;
//利用递归构造左孩子的树
//参数中,先序左节点位置加一寻找下一个根节点,右节点为左节点的位置初值加上左节点个数,中序右节点为根节点减一
root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left, inorder_left, inorder_root - 1);
//递归构造右孩子的树
//参数中,先序左节点为左节点初值位置加上左节点个数加1,中序左节点为根节点位置加1
root->right = myBuildTree(preorder, inorder, preorder_left + size_left + 1, preorder_right, inorder_root + 1, inorder_right);
return root;
}
};
(简单)
6.用两个栈实现一个队列
队列的声明如下,请实现它的两个函数appendTail和deleteHead,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead操作返回-1)
例:
输入:
1 , 2 , 3 , 4 , 5
输出:
5 , 4 , 3 , 2 , 1
栈的特点是先进后出,队列的特点是先进先出
用两个栈来实现队列,即由一个栈来输入队列中的元素,最先输入的元素会在栈底,再建一个栈来输出元素,把输入栈的元素输出到第二个栈,那么最后输入的元素会在第二个栈的栈底,当第二个栈进行输出时就相当于队列的输出,后来的最后输出
class Solution
{
public:
//定义两个栈,输入和输出元素
stack<int>inStack, outStack;
int outDate(stack<int>inStack, stack<int>outStack)
{
for (int i = 1; i < 6; i++)
{
//为输入栈赋值
inStack.push(i);
}
//若输入栈为空则停止
while (inStack.empty())
{
//将输入栈的元素从栈顶开始输入到输出栈
outStack.push(inStack.top());
//删除输入栈的元素
inStack.pop();
}
for (int i = 1; i < 6; i++)
{
int date = outStack.top();
outStack.pop();
return date;
}
}
};
(简单)
7.斐波那契数列
写一个函数,输入n,求斐波那契数列的第n项。斐波那契数列由0和1开始,之后的斐波那契数就是由之前的两数相加而得出。
例:
斐波那契数列:
0 , 1 , 1 , 2 , 3 , 5 , 8 , 13 , 21
斐波那契数列就是由两个数相加得到第三个数,可通过迭代的方法求出
int Fibonacci(int n)
{
//x为判断要查找的斐波那契数是单数还是双数
//y为计量查找的轮数
//i和j为斐波那契初始数
int x = 0, y = 0, i = 0, j = 1;
if (n % 2 != 0)
{
x = n + 1;
}
else
{
x = n;
}
//这里为主要实现,采用迭代的方法,为i和j不断重新赋值
for ( i = 0, j = 1; y < (x / 2) - 1; y++)
{
i = i + j;
j = i + j;
}
if (x % 2 == 0)
{
return j;
}
else
{
return i;
}
}
(简单)
8. 青蛙跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上n级台阶总共有多少种跳法
例:
输入:n=1
输出:1
·
输入:n=0
输出:1
·
输入:n=7
输出:21
青蛙跳上第n级台阶时,只有两种跳法,即跳1级或2级
当为1级跳法,则表示跳到n级台阶有F(n-1)种跳法
当为2级跳法,则表示跳到n级台阶有F(n-2)种跳法
由此跳到n级台阶就有F(n)=F(n-1)+F(n-2)种跳法
这就是一个斐波那契数列(前一个加上后一个数得出第三个数),与斐波那契数列的不同点在于斐波那契数列是从0,1开始的,这个是由1,1开始的(条件0级台阶为1种跳法)
(简单)
9.旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
给你一个可能存在重复元素元素值的数组numbers,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。
例如:
数组【3,4,5,1,2】为【1,2,3,4,5】的一次旋转,该数组的最小值为1
注意:
数组【a[0],a[1],······a[n-1]】旋转一次的结果为数组【a[n-1],a[0],a[1]······,a[n-2]】
int main()
{
vector<int>v;
Spin(v, 5, 2);
system("pause");
return 0;
}
//n为要插入的元素个数,q为要进行的旋转次数
void Spin(vector<int>v, int n, int q)
{
//利用动态数组对元素进行升序插入
for (int i = 0; i < n; i++)
{
v.push_back(i);
}
//x为对最后一个元素的寄存,y为对旋转次数的计量
int x, y = 0;
while (y < q)
{
x = v[n-1];
//从倒数第二个元素开始,从后逐个往前移
for (int a = n - 2; a > -1; a--)
{
v[a + 1] = v[a];
}
//再把最后一个元素插入到第一个进行旋转
v[0] = x;
y++;
}
//可以发现,旋转的次数就是v[0]即第一个元素的后移次数
//由于是升序,所以第一个元素肯定最小,找到第一个元素的位置就是最小元素
cout << v[y] << endl;
}
(中等)
10.矩阵中的路径
给定一个m*n二维字符网格board和一个字符串单词word。如果word存在于网格中,返回true,否则返回false
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一单元格内的字母不允许被重复使用
例:
A B C E S F C S A D E E 输入:
board = 【A , B , C , E 】,【S , F , C , S 】,【A , D , E , E】,word = ABCCED
输出:true
·
输入:
board = 【a , b 】,【c,d】,word=abcd
输出:false
即不能直接连续几行或几列输入word,只能从网格中连续横向,列向和横向加上列向输入
int Matrix(vector<vector<char>>v, int x, int y,string q)
{
cout << "输入网格的值" << endl;
char date;
//为网格赋值
for (int a = 0; a < x; a++)
{
for (int b = 0; b < y; b++)
{
cout << "--" << endl;
cin >> date;
v[a][b] = date;
}
}
//设置两个量来记录坐标的变化
int k1, z1 = 0;
//对字符串的每个元素进行查找对比
for (int i = 0; i < q.size(); i++)
{
//对每个值进行网络内的遍历查找,返回其坐标
int k,z = Search(v, q[i],x,y);
//若找不到值则直接返回FALSE
if (k == 0 && z == 0)
{
return FALSE;
}
//后面的值的坐标与前面的值的坐标对比
//只有与横坐标相邻,纵坐标相邻的元素才是合格的,第一个元素不需要对比只需要查找
if (i != 0 && (k1 - k != 1 || k1 - k != -1) && (z1 - z != 1 || z1 - z != -1))
{
return FALSE;
}
k1 = k;
z1 = z;
}
return TRUE;
}
int Search(vector<vector<char>>v1, char p,int x,int y)
{
for (int i = 0; i < x; i++)
{
for (int j = 0; j < y; j++)
{
if (v1[i][j] == p)
{
return i, j;
}
}
}
return 0, 0;
}
这是一种暴力的解法,从字符串的第一个元素开始,每个元素都需要经历查找,再对比上个元素的位置,只有相邻才继续遍历,所以时间复杂度会较高
若想要较低的时间复杂度,可以考虑用递归