这段时间写完了力扣的剑指offer,写几篇博客记录一下解题思路和代码。
offer 09 用两个栈实现队列
这道题比较基础,我们知道栈的特点是先进后出,队列的特点是先进先出,刚好相反。再用两个栈模拟队列的时候,可以进行以下操作
假设两个栈的名字为A,B
1.模拟 入队时,向A栈入栈
2. 模拟出栈时,将A栈的所有内容出栈,并且依次压入B栈,之后弹出B栈的第一个元素,再将B栈的所有内容出栈,依次压入A栈。
每次出栈根据上述流程,可以完成用两个栈模拟队列。
class CQueue {
Stack<Integer> s1;
Stack<Integer> s2;
public CQueue() {
this.s1 = new Stack<Integer>();
this.s2 = new Stack<Integer>();
}
public void appendTail(int value) {
s1.push(value);
return;
}
public int deleteHead() {
if(s1.empty())
return -1;
while(!s1.empty())
{
s2.push((int)s1.pop());
}
int result = (int)s2.pop();
while(!s2.empty())
{
s1.push((int)s2.pop());
}
return result;
}
}
/**
* Your CQueue object will be instantiated and called as such:
* CQueue obj = new CQueue();
* obj.appendTail(value);
* int param_2 = obj.deleteHead();
*/
offer 10-I 斐波那契数列
计算斐波那契数列的第n项也是一种基础类型的题目,但是这种题目还是存在一点优化的空间。就是不用记录斐波那契数列所求的值之前的每一个值,只需要不断地更新两个值就好,可以节省内存的开销。
斐波那契的基本公式:f(n) = f(n - 1) + f(n - 2)
斐波那契数列的初始情况:f(0) = 0,f(1) = 1
class Solution {
public int fib(int n) {
if(n == 0)
return 0;
else if(n == 1)
return 1;
else
{
int a[] = {0,1};
for(int i = 2;i <= n;i++)
{
//此处取余是题目要求
int temp = (a[0] + a[1]) % 1000000007;
a[0] = a[1];
a[1] = temp;
}
return a[1];
}
}
}
offer 03 数组里的重复数字
这道题的解法比较多,这里介绍两种。
- 将原来的数组排序,然后从头便利,如果nums[i] == nums[i + 1],则表示有两个数字重复,直接返回答案。该方法时间复杂度为o(nlogn),空间复杂度为o(1)。(ps:最好情况下)
- 哈希表,每个数组元素的值就是他们的哈希值,先寻找数组中的最大值确定数组边界,建立一个新的数组,之后以他们的哈希值为索引放在对应的数组元素中,如果在放置的过程中发现这个数组元素已经被赋值,就返回该数组元素的索引。该方法时间复杂度为o(n),空间复杂度为o(n)。与1方法对比,是用空间换时间。
下面的代码是第一种解法的代码。
class Solution {
public int findRepeatNumber(int[] nums) {
Arrays.sort(nums);
for(int i = 0;i < nums.length;i++)
{
if(nums[i] == nums[i + 1])
return nums[i];
}
return 1;
}
}
offer04 二维数组中的查找
这道题所给出的二维数组的规律是:若假设二维数组是a[i][j],随着i增大,数组元素变大,随着j增大,数组元素也变大。所以为了在二维数组中高效的查找,就要跳出二维数组只能从第一个元素搜索这个思路。
如果从第一个元素搜索,无论是i增加还是j增加,元素的大小都是增加,反之都是减小。所以我们并不能知道两个增长的方向应该向哪个方向搜索。但是如果我们从二维数组的左下角为起点开始搜索(假设二维数组样子是矩形),我们发现,i减小和j增大分别对应着数组元素的减小和增大。
所以如果用这个元素和目标数字比较,如果目标数字大了,就向左移动,如果小了,就向上移动。在O(n)的时间复杂度内就可以找到目标数字。
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
int xMax = matrix.length - 1;
if(xMax < 0)
return false;
int yMax = matrix[0].length - 1;
int x = 0,y = yMax;
while(x <= xMax&&y >= 0)
{
if(matrix[x][y] == target)
{
return true;
}
else if(matrix[x][y] > target)
{
y--;
continue;
}
else
{
x++;
continue;
}
}
System.out.println("没找到");
return false;
}
}
offer10-II 青蛙跳台阶问题
这道题是一道动态规划问题,很巧的是它可以使用斐波那契数列轻松解决。我们先从动态规划的角度看这道题,显然,如果一级台阶只有一种跳法,两级台阶有两种跳法。我们设函数f(n)为n级台阶的情况下的跳法数量。
f(1) = 1
f(2) = 2。
现在要计算n级台阶有多少种跳法,用动态规划的思想可以知道,跳上n级台阶,可以从n-2级台阶一次挑两个到达,也可以从n-1级台阶,跳一级到达。
所以f(n) = f(n-1) + f(n-2)。
这个时候就有一个问题,为什么从n-2级台阶不能向上跳两次然后到达n级呢,是不是少算了一个?
其实并不是,从n-2级跳两次到达n级,就相当于从n-1级跳一级到达n级,所以我们已经算过了。
这个时候发现通过动态规划算出来的递推式刚好和斐波那契数列的递推式相同。
class Solution {
public int numWays(int n) {
if(n == 0)
return 1;
int f1 = 1,f2 = 2;
if(n == 1)
return f1;
else if(n == 2)
return f2;
for(int i = 0;i < n - 2;i++)
{
int temp = f2;
f2 =f1 + f2;
f1 = temp;
f1 = f1 % 1000000007;
f2 = f2 % 1000000007;
}
return f2%1000000007;
}
}
offer11 旋转数组的最小数字
这道题是将一个有序数组的后部分提到前面来,然后寻找数组的最小值。我们可以直接寻找,因为数组的后半部分是有序的,所以我们只需要从数组头部寻找,找到第一个减小的数字就是答案。当然,答案数字也有一个特点是他的左右两个数字都比他大,其他位置的数字都没有这个特点。所以也可以通过这个特点进行二分查找。
class Solution {
public int minArray(int[] numbers) {
int min = numbers.length - 1;
while(min >= 0)
{
if(min > 0&&numbers[min] >= numbers[min - 1])
min--;
else
return numbers[min];
}
return 1;
}
}
offer12 矩阵中的路径
这道题可以采用dfs的方法,在开始的时候,遍历整个数组,当搜索到与给定字符串第一个字符相同的位置时,开始以此为起点向周围的所有方向搜索。
在搜索过程中如果遇到这个方向搜索会越界,或者这个位置已经被搜索过(设置visited数组,并且在搜索的过程中改变他),或者这个位置的字符或现在需要搜索的字符不同这几种情况之一,都不能继续搜索(没有意义),并且返回false。
及时进行剪枝可以缩短代码执行的时间。如果在搜索过程中计数到了所给字符串的长度,就可以结束搜索并且返回true。
有一个关键步骤是在递归函数到达一个节点时,要将这个节点的visited设置为true,然后在递归函数返回后,要将visited函数重新还原为false。这一点在注释里也有体现。
class Solution {
public boolean exist(char[][] board, String word) {
for(int i = 0;i < board.length;i++)
{
for(int j = 0;j < board[0].length;j++)
{
if(board[i][j] == word.charAt(0))//第一个值相等,开始匹配
{
boolean[][] visited = new boolean[board.length][board[0].length];
for(int i1 = 0;i1 < visited.length;i1++)
for(int j1 = 0;j1 < visited[0].length;j1++)
visited[i1][j1] = false;
if(find(board,word,visited,i,j,0))
{
System.out.println("找到了");
return true;
}
}
}
}
System.out.println("没找到");
return false;
}
boolean find(char[][]board,String word,boolean[][] visited,int x,int y,int num)//num是第几个
{
if(x >= board.length||x < 0||y >= board[0].length||y < 0||board[x][y] != word.charAt(num)||visited[x][y])
return false;
if(num == word.length() - 1)
return true;
boolean result = false;
visited[x][y] = true;//当前点访问过,如果这条路走不通,函数再次返回到这里,需要将其还原为false
result = find(board,word,visited,x + 1,y,num + 1)||
find(board,word,visited,x ,y + 1,num + 1)||
find(board,word,visited,x - 1,y,num + 1)||
find(board,word,visited,x,y - 1,num + 1);
//这里将visited数组恢复原样很重要!!
visited[x][y] = false;
return result;
}
}
offer05 替换空格
这道题很简单,只需要重新建立一个字符串,再遍历原字符串,遇到空格就向新字符串添加%20,反之就添加原字符串的字符。
class Solution:
def replaceSpace(self, s: str) -> str:
result = ""
for i in range(len(s)):
if s[i] != ' ':
result += s[i]
else:
result += "%20"
return result
offer13 机器人的运动范围
这道题也同样使用dfs的方法,从(0,0)点出发,每次探索身边的上下左右四个位置(如果不会越界),在每次到达一个点之后,判断这个点的坐标是否符合要求,如果符合的话,把这个点在canVisited数组中设置为true。这个设置很重要,因为可能出现这种情况:一个点满足各数位之和小于k,但是这个点周围的点都不满足,这个时候这个点实际上是不能到达的。如果不设置canvisited条件,单单是遍历整个数组寻找,会把这个不该算进去的点算入答案。
class Solution {
public int movingCount(int m, int n, int k) {
int result = 0;
boolean[][] canVisited = new boolean[m][n];
for(int i = 0;i < m;i++)
for(int j = 0;j < n;j++)
canVisited[i][j] = false;
canVisited[0][0] = true;
for(int i = 0;i < m;i++)
{
for(int j = 0;j < n;j++)
{
if(canIn(i,j,k)&&canVisited[i][j])
{
result++;
if(i > 0&&!canVisited[i - 1][j])
canVisited[i - 1][j] = true;
if(i <= m - 2&&!canVisited[i + 1][j])
canVisited[i + 1][j] = true;
if(j > 0&&!canVisited[i][j - 1])
canVisited[i][j - 1] = true;
if(j <= n - 2&&!canVisited[i][j + 1])
canVisited[i][j + 1] = true;
}
else
continue;
}
}
System.out.println(result);
return result;
}
boolean canIn(int x,int y,int k)
{
int sum = 0;
while(x > 0||y > 0)
{
if(x > 0)
{
sum += x % 10;
x /= 10;
}
if(y > 0)
{
sum += y % 10;
y /= 10;
}
}
if(sum <= k)
return true;
else
return false;
}
}
offer06 从尾到头打印链表
将一个链表从尾到头打印,可以联想到栈的特性。将链表从头到尾遍历,遍历过程中依次压入栈中,最后将栈中的数字依次弹出,写入数组中返回即可。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* reversePrint(struct ListNode* head, int* returnSize){
int size = 0;
int stack[10000] = {0};
while(head != NULL)
{
stack[size++] = head->val;
head = head->next;
}
printf("%d\n",size);
int* result = (int*)malloc(sizeof(int) * size);
*returnSize = size;
int a = size - 1;
for(int i = 0;i < size;i++)
{
printf("stack值%d\n",stack[a]);
result[i] = stack[a];
a--;
}
return result;
}
offer07 重建二叉树
这道题是数据结构学习过程中很经典的一道题。根据二叉树的性质,可以通过 前序 + 中序或者中序 + 后序的方法还原之前的二叉树。
根据前序遍历的特点:根 左 右,还有中序遍历的特点:左 根 右
我们可以在前序遍历中得到树的根(第一个节点),然后再中序遍历中寻找根节点(可能在中间某个位置),这样就可以通过根节点把二叉树的中序遍历分成两部分,左边是二叉树的左子树的中序遍历,右边是二叉树的右子树的中序遍历。
通过上述的方法结合递归的思想,可以实现下面的思路:
- 在前序遍历中寻找根节点(前序遍历的第一个节点)
- 在中序遍历中搜索根节点的位置,将二叉树分为左右子树,并且记录其在中序遍历中的下标范围。
- 对于左右子树,如果判断只有一个结点,就将他封装成结点,然后与根节点建立连接,如果左右子树不只有一个节点,可以获得左子树的前序遍历和中序遍历内容,但后递归进行以上的相同操作,右子树同理。
在左右子树不只有一个结点的情况下,对于中序遍历,我们很容易通过根节点的位置分理出左右子树的中序遍历,那也就是说可以知道左右子树各有多少个节点。之后就可以根据节点数量,在前序遍历中获得左右子树的前序遍历内容。这对我们在递归求解子树的结构十分重要。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder.length == 0)
{
return null;
}
int start = 0,end = preorder.length - 1;
TreeNode root = new TreeNode(preorder[0]);
root.left = null;
root.right = null;
buildTree(root,preorder,inorder,start,end);
pre(root);
return root;
}
static void pre(TreeNode root)
{
System.out.println(root.val);
if(root.left != null)
pre(root.left);
if(root.right != null)
pre(root.right);
}
static void buildTree(TreeNode root,int[] preorder,int[] inorder,int start,int end)
{
if(start > end)
return;
/*左子树起止位置*/
int leftStart = start;
int leftEnd = findIndex(root.val,inorder) - 1;
/*右子树起止位置*/
int rightStart = leftEnd + 2;
int rightEnd = end;
/*如果有多个元素,将左子树根节点作为新的根节点,递归*/
if(leftStart != leftEnd&&leftStart <= leftEnd)
{
TreeNode leftRoot = new TreeNode(preorder[findIndex(root.val,preorder) + 1]);
leftRoot.left = null;
leftRoot.right = null;
root.left = leftRoot;//建立联系
buildTree(leftRoot,preorder,inorder,leftStart,leftEnd);
}
/*如果有多个元素,将右子树根节点作为新的根节点,递归*/
if(rightStart != rightEnd&&rightStart <= rightEnd)
{
TreeNode rightRoot = new TreeNode(preorder[findIndex(root.val,preorder) + leftEnd - leftStart + 2]);
rightRoot.left = null;
rightRoot.right = null;
root.right = rightRoot;//建立联系
buildTree(rightRoot,preorder,inorder,rightStart,rightEnd);
}
/*如果左子树只有一个元素*/
if(leftStart == leftEnd)
{
TreeNode leftLeaf = new TreeNode(inorder[leftEnd]);
leftLeaf.left = null;
leftLeaf.right = null;
root.left = leftLeaf;//建立联系
}
if(rightStart == rightEnd)
{
TreeNode rightleaf = new TreeNode(inorder[rightEnd]);
rightleaf.left = null;
rightleaf.right = null;
root.right = rightleaf;//建立联系
}
return;
}
//寻找target在前序或中序遍历中的位置
static int findIndex(int target, int[] order)
{
for(int i = 0;i < order.length;i++)
{
if(order[i] == target)
return i;
}
return -1;
}
}
offer14-I 剪绳子
这道题在一开始的时候我觉得可以用函数去做,确实可以写出将绳子分成n段之后乘积的函数,之后通过求导可以算出函数的驻点,之后通过驻点求最大值。
不过这样做有一个很大的问题,由驻点(设为分的段数,我们可以想到应该将绳子尽可能切的一样长)算出的每段绳子的长度通常不是整数。这不符合题目的要求。
但是开始我想的办法是可以对驻点的值分别向上和向下,然后比较两个值算出的乘积的大小。这样做可以通过一部分的测试样例(28/50),但是这样做会出现问题。
举个例子:假如绳子的长度为22,根据求导将它分为8.09段时乘积达到最大,所以我们要计算将绳子分为7,8,9段时乘积的大小。通过计算在绳子被分为7段的时候答案为2187,可见是将绳子分为七个3和一个1,这显然没有将绳子分为六个3和两个2所得到的2916大。
造成这个问题的原因就是我的算法将绳子一直以相同的长度再切,最后如果剩下1,这一段不能对乘积做出任何贡献。但是这样确实符合了我的设定 “将最多段绳子分程一样长的段” 这一条件。
在查看题解之后,我发现这道题用了一个数论上的定理:任何一个大于2的整数都可以分为若干个2和3之和,其中3越多,乘积越大。
(这东西不知道不好做啊)知道这个定理,程序就变得简单了很多。
class Solution {
public int cuttingRope(int n) {
int result = 1;
if(n == 2)
{
System.out.println(1);
return 1;
}
else if(n == 3)
{
System.out.println(2);
return 2;
}
else
{
if(n % 3 == 1)
{
while(n != 4)
{
result *= 3;
n -= 3;
}
result *= 4;
System.out.println(result);
return result;
}
else
{
while(n >= 3)
{
result *= 3;
n -= 3;
}
if(n == 2)
{
result *= n;
System.out.println(result);
return result;
}
else
{
System.out.println(result);
return result;
}
}
}
}
}
offer14-II 剪绳子II
这道题和上一道题的区别在于这道题数据量比较大,使用int类型会越界,所以使用long类型来存储数据,之后再将其转为int就可以了。
offer15 二进制中1的个数
这道题可以利用数据在计算机中的储存形式来做,数据在计算机中以二进制的形式储存,所以每次只需要将数据的低位与0x01(就是1)相与,如果结果为1,说明这个数字最后一位是1。然后将数字逻辑右移一位。直到数字右移成0为止。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int result = 0;
while(n != 0)
{
if((n & 1) == 1)
result++;
n >>>= 1;
}
return result;
}
}
offer16 数值的整数次方
这道题如果使用单纯的循环(On时间复杂度)是一定会超时的,所以我们采用快速幂的方法。这里只讨论X^n,n为正整数的情况,n为负数的情况可以转化为n是正数的情况。
下面说明快速幂。
快速幂的时间复杂度是Ologn,它利用了二进制的特点。比如,在计算(3^5)时,利用二进制可以将其写成 (3^101)。(101为二进制的3)
我们可以进行如下的推导:
我们可以看到,在二进制位为0的时候,运算式实际上乘的是1,只有在二进制位为1的时候,所乘的数字有实际的贡献。所以我们可以遍历n的二进制所有位数,在第i位为为1的时候,将3(1*2^(i-1))乘到答案里。在第i位为0的时候,仅仅更新要乘进去的数字即可。更新的方法为每次将该数字平方,相当于给他的阶数乘2,这一步可以手推一下上面给出的三的五次方的例子就可以理解。
class Solution {
public double myPow(double x, int n) {
long b = n;
double res = 1;
if(b < 0)
{
b = -b;
x = 1 / x;
}
while(b > 0)
{
if((b & 1) == 1)//b的二进制形式最右端为1,将数字乘进去
{
res *= x;
}
//更新要乘进去的数字
x *= x;
//将b右移,这里b已经被变为正数,所以不用逻辑右移
b >>= 1;
}
System.out.println(res);
return res;
}
}