3月1日
使用队列实现栈的下列操作:
- push(x) -- 元素 x 入栈
- pop() -- 移除栈顶元素
- top() -- 获取栈顶元素
- empty() -- 返回栈是否为空
注意:
你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。
你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)。
这道题,我的做法不是最简单的做法,但是是最容易理解的做法。
思路:
一个队列加入元素,弹出元素时,需要把队列中的 元素放到另外一个队列中,弹出最后一个元素
两个队列始终保持只有一个队列是有数据的
class MyStack {
Queue<Integer> queueA;
Queue<Integer> queueB;
/** Initialize your data structure here. */
public MyStack() {
queueA = new LinkedList<>();
queueB = new LinkedList<>();
}
/** Push element x onto stack. */
public void push(int x) {
if (!queueA.isEmpty()) {
queueA.offer(x);
} else {
queueB.offer(x);
}
}
/** Removes the element on top of the stack and returns that element. */
public int pop() {
if (!queueA.isEmpty()) {
while (queueA.size() > 1) {
queueB.offer(queueA.poll());
}
return queueA.poll();
} else {
while (queueB.size() > 1) {
queueA.offer(queueB.poll());
}
return queueB.poll();
}
}
/** Get the top element. */
public int top() {
int top = 0;
if (!queueA.isEmpty()) {
while (queueA.size() > 1) {
queueB.offer(queueA.poll());
}
top = queueA.poll();
queueB.offer(top);
} else {
while (queueB.size() > 1) {
queueA.offer(queueB.poll());
}
top = queueB.poll();
queueA.offer(top);
}
return top;
}
/** Returns whether the stack is empty. */
public boolean empty() {
return queueA.isEmpty() && queueB.isEmpty();
}
}
3月2日
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
经典题,不多BB。重点是对递归法的理解。
public class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
//这里的cur就是最后一个节点
ListNode p = reverseList(head.next);
//如果链表是 1->2->3->4->5,那么此时的cur就是5
//而head是4,head的下一个是5,下下一个是空
//所以head.next.next 就是5->4
head.next.next = head;
//断开当前连接
head.next = null;
//每层递归函数都返回cur,也就是最后一个节点
return p;
}
}
3月3日
给定两个排序后的数组 A 和 B,其中 A 的末端有足够的缓冲空间容纳 B。 编写一个方法,将 B 合并入 A 并排序。
初始化 A 和 B 的元素数量分别为 m 和 n。
示例:
输入:
A = [1,2,3,0,0,0], m = 3
B = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
public class Solution {
public void merge(int[] A, int m, int[] B, int n) {
//方法1:利用Collections.sort进行排序(执行用时:3 ms)
/*
List<Integer> list = new ArrayList<>();
for (int i = 0; i < m; i++) {
list.add(A[i]);
}
for (int i = 0; i < n; i++) {
list.add(B[i]);
}
Collections.sort(list);
for (int i = 0; i < m + n; i++) {
A[i] = list.get(i);
}
*/
//方法2:利用Arrays.sort进行排序(执行用时:1 ms)
/*
for (int i = 0; i < n; i++) {
A[m + i] = B[i];
}
Arrays.sort(A);
*/
//方法3:逆向双指针(执行用时:0 ms)
int i = m - 1, j = n - 1, idx = m + n - 1;
while (j >= 0) {
if (i < 0 || B[j] >= A[i]) {
A[idx--] = B[j--];
} else {
A[idx--] = A[i--];
}
}
}
}
3月4日
在给定的网格中,每个单元格可以有以下三个值之一:
- 值 0 代表空单元格;
- 值 1 代表新鲜橘子;
- 值 2 代表腐烂的橘子。
每分钟,任何与腐烂的橘子(在 4 个正方向上)相邻的新鲜橘子都会腐烂。
返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。
示例 1:
输入:[[2,1,1],[1,1,0],[0,1,1]]
输出:4
经典的广度优先搜索问题,代码如下:
public class Solution {
//分别对应 左、下、右、上 四个方向
int[] dr = new int[]{-1, 0, 1, 0}; //行方向
int[] dc = new int[]{0, -1, 0, 1}; //列方向
public int orangesRotting(int[][] grid) {
int R = grid.length, C = grid[0].length;
// queue : all starting cells with rotten oranges
Queue<Integer> queue = new ArrayDeque();
Map<Integer, Integer> depth = new HashMap(); //key: 位置 value: 时间
//初始化
for (int r = 0; r < R; ++r)
for (int c = 0; c < C; ++c)
if (grid[r][c] == 2) {
int code = r * C + c; //位置(index)
queue.add(code);
depth.put(code, 0);
}
int ans = 0;
while (!queue.isEmpty()) { //借助队列进行广度优先搜素
int code = queue.remove();
int r = code / C, c = code % C;
for (int k = 0; k < 4; ++k) { //遍历四个方向
int nr = r + dr[k];
int nc = c + dc[k];
if (0 <= nr && nr < R && 0 <= nc && nc < C && grid[nr][nc] == 1) {
grid[nr][nc] = 2;
int ncode = nr * C + nc;
queue.add(ncode);
depth.put(ncode, depth.get(code) + 1);
ans = depth.get(ncode);
}
}
}
for (int[] row: grid) {
for (int v: row) {
if (v == 1) {
return -1;
}
}
}
return ans;
}
}
3月5日
排排坐,分糖果。
我们买了一些糖果 candies,打算把它们分给排好队的 n = num_people 个小朋友。
给第一个小朋友 1 颗糖果,第二个小朋友 2 颗,依此类推,直到给最后一个小朋友 n 颗糖果。
然后,我们再回到队伍的起点,给第一个小朋友 n + 1 颗糖果,第二个小朋友 n + 2 颗,依此类推,直到给最后一个小朋友 2 * n 颗糖果。
重复上述过程(每次都比上一次多给出一颗糖果,当到达队伍终点后再次从队伍起点开始),直到我们分完所有的糖果。注意,就算我们手中的剩下糖果数不够(不比前一次发出的糖果多),这些糖果也会全部发给当前的小朋友。
返回一个长度为 num_people、元素之和为 candies 的数组,以表示糖果的最终分发情况(即 ans[i] 表示第 i 个小朋友分到的糖果数)。
示例 1:
输入:candies = 7, num_people = 4
输出:[1,2,3,1]
解释:
第一次,ans[0] += 1,数组变为 [1,0,0,0]。
第二次,ans[1] += 2,数组变为 [1,2,0,0]。
第三次,ans[2] += 3,数组变为 [1,2,3,0]。
第四次,ans[3] += 1(因为此时只剩下 1 颗糖果),最终数组变为 [1,2,3,1]。
class Solution {
public int[] distributeCandies(int candies, int num_people) {
int[] res = new int[num_people];
int index = 0;
while (candies > 0) {
for (int i = 0; i < num_people; i++) {
int sum = index * num_people + (i + 1);
if (candies > sum) {
res[i] += sum;
candies -= sum;
} else {
res[i] += candies;
candies = 0;
}
}
index++;
}
return res;
}
}
3月6日
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
示例 1:
输入:target = 9
输出:[[2,3,4],[4,5]]
示例 2:
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
这道题可以通过暴力来解,但是最好的做法是采用滑动窗口的方式。
滑动窗口可以看成数组中框起来的一个部分。在一些数组类题目中,我们可以用滑动窗口来观察可能的候选结果。当滑动窗口从数组的左边滑到了右边,我们就可以从所有的候选结果中找到最优的结果。
- 当窗口的和小于 target 的时候,窗口的和需要增加,所以要扩大窗口,窗口的右边界向右移动
- 当窗口的和大于 target 的时候,窗口的和需要减少,所以要缩小窗口,窗口的左边界向右移动
- 当窗口的和恰好等于 target 的时候,我们需要记录此时的结果。设此时的窗口为 [i, j)[i,j),那么我们已经找到了一个 ii 开头的序列,也是唯一一个 ii 开头的序列,接下来需要找 i+1i+1 开头的序列,所以窗口的左边界要向右移动
class Solution {
public int[][] findContinuousSequence(int target) {
int i = 1; // 滑动窗口的左边界
int j = 1; // 滑动窗口的右边界
int sum = 0; // 滑动窗口中数字的和
List<int[]> res = new ArrayList<>(); //注意写法:目的是为了方便将List转换成二维数组
while (i <= target / 2) {
if (sum < target) {
// 右边界向右移动
sum += j;
j++;
} else if (sum > target) {
// 左边界向右移动
sum -= i;
i++;
} else {
// 记录结果
int[] arr = new int[j-i];
for (int k = i; k < j; k++) {
arr[k-i] = k;
}
res.add(arr);
// 左边界向右移动
sum -= i;
i++;
}
}
return res.toArray(new int[res.size()][]);
}
}
3月7日
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
示例 1:
输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
这道题主要采用 队列 + 双端队列 的方式。
class MaxQueue {
Queue<Integer> queue;
Deque<Integer> maxQueue; //双端队列(也需要存全部元素,只不过是单调递减的)
public MaxQueue() {
queue = new ArrayDeque();
maxQueue = new ArrayDeque();
}
public int max_value() {
return maxQueue.isEmpty() ? -1 : maxQueue.peek();
}
public void push_back(int value) {
queue.add(value);
while(!maxQueue.isEmpty() && value > maxQueue.getLast()) {
maxQueue.pollLast(); //从尾部把小于当前value的全部删除(因为用不到了)
}
maxQueue.add(value);
}
public int pop_front() {
if(queue.isEmpty())
return -1;
int ans = queue.poll();
if(ans == maxQueue.peek()) {
maxQueue.poll();
}
return ans;
}
}
3月8日
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
这道题是典型的的DP问题,当然也可以使用DFS求解。
public class Solution {
//动态规划
public int coinChange(int[] coins, int amount) {
int dp[] = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
//反向dp
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
//DFS + 剪枝
int res = Integer.MAX_VALUE;
public int coinChange(int[] coins, int amount) {
Arrays.sort(coins);
dfs(coins, coins.length - 1, amount, 0); //用最大的面额作为初始条件
return res == Integer.MAX_VALUE ? -1 : res;
}
public void dfs(int[] coins, int index, int amount, int cnt){
if(index < 0){
return;
}
for(int c = amount/coins[index]; c >= 0; c--){
int na = amount - c * coins[index]; //剩下的面额
int ncnt = cnt + c; //目前使用的硬币数
if(na == 0){ //剩余的面额为0(符合)
res = Math.min(res, ncnt);
break; //剪枝1
}
if(ncnt + 1 >= res){ //使用的硬币数过多,已无法满足最优的要求(舍弃)
break; //剪枝2
}
dfs(coins, index-1, na, ncnt);
}
}
}
3月9日
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意:利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
这道题较为暴力的方式是使用两重循环,其实可以使用一重循环进行改进,代码如下:
public class Solution {
public int maxProfit(int[] prices) {
int min = Integer.MAX_VALUE;
int maxProfit = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < min) {
min = prices[i];
} else if (prices[i] - min > maxProfit) {
maxProfit = prices[i] - min;
}
}
return maxProfit;
}
}
3月10日
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过根结点。
示例 :
给定二叉树
1
/ \
2 3
/ \
4 5
这道题有个坑。。注意:最大值不一定包含根节点。但是一定是经过某个节点,可以使用 DFS。
class Solution {
int res = 0;
public int diameterOfBinaryTree(TreeNode root) {
dfs(root);
return res;
}
// 函数dfs的作用:找到以root为根节点的二叉树的最大深度
private int dfs(TreeNode root){
if(root == null){
return 0;
}
int leftDepth = dfs(root.left);
int rightDepth = dfs(root.right);
res = Math.max(res, leftDepth + rightDepth);
return Math.max(leftDepth, rightDepth) + 1;
}
}
3月14日
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
public class Solution {
//动态规划法
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
//dp[j]代表nums [0…j] 中以 nums[j] 结尾的最长上升子序列的长度
dp[0] = 1;
int maxans = 1;
for (int i = 1; i < dp.length; i++) {
int maxval = 0;
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
maxval = Math.max(maxval, dp[j]);
}
}
dp[i] = maxval + 1;
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
//贪心 + 二分查找
public int lengthOfLIS1(int[] nums) {
/**
由定义知dp数组必然是一个递增数组, 可以用 maxL 来表示最长递增子序列的长度.
对数组进行迭代, 依次判断每个数num将其插入dp数组相应的位置:
1. num > dp[maxL], 表示num比所有已知递增序列的尾数都大, 将num添加入dp
数组尾部, 并将最长递增序列长度maxL加1
2. dp[i-1] < num <= dp[i], 只更新相应的dp[i]
**/
int maxL = 0; //最长递增子序列的长度
int[] dp = new int[nums.length];
//dp[i]: 所有长度为i+1的递增子序列中, 最小的那个序列尾数
for(int num : nums) {
// 二分法查找, 也可以调用库函数如binary_search
int lo = 0, hi = maxL;
while(lo < hi) {
int mid = lo + (hi - lo) / 2;
if(dp[mid] < num) {
lo = mid+1;
} else {
hi = mid;
}
}
dp[lo] = num;
if(lo == maxL) {
maxL++;
}
}
return maxL;
}
}
持续更新中……