算法题目及解答
圆圈中最后剩下的数
问题描述
0,1,,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。
解答思路
针对这个问题monica提供了两种解题思路
- 这个问题可以使用循环链表来解决。我们可以先创建一个循环链表,将0到n-1这n个数字依次加入链表中。然后从头节点开始,每次找到第m个节点并删除,直到链表中只剩下一个节点为止。这个最后剩下的节点就是答案。
- 这个问题实际上是著名的约瑟夫环问题,可以使用递推或数学公式来解决
参考代码
循环链表
public static int lastRemaining(int n, int m) {
ListNode head = new ListNode(0);
ListNode cur = head;
for (int i = 1; i < n; i++) {
cur.next = new ListNode(i);
cur = cur.next;
}
cur.next = head;
while (cur.next != cur) {
for (int i = 1; i < m; i++) {
cur = cur.next;
}
cur.next = cur.next.next;
}
return cur.val;
}
static class ListNode {
int val;
ListNode next;
public ListNode(int val) {
this.val = val;
}
}
其中ListNode是链表节点的定义,lastRemaining方法接受两个参数n和m,分别表示数字个数和每次删除的数字的位置。该方法返回最后剩下的数字。
递推方式
public static int lastRemaining(int n, int m) {
int ans = 0;
for (int i = 2; i <= n; i++) {
ans = (ans + m) % i;
}
return ans;
}
其中n和m的含义同上。该方法返回最后剩下的数字。
两种方式的比较
- 第一个回答使用循环链表来模拟删除的过程,时间复杂度为O(nm),空间复杂度为O(n)
- 第二个回答是经典的约瑟夫环问题的解法,时间复杂度为O(n),空间复杂度为O(1)
- 如果n和m的值比较小,使用第一个回答的方法可能更加简单。但如果n和m的值比较大,使用第二个回答的方法会更加高效。
解码方法
问题描述
一条包含字母 A-Z 的消息通过以下方式进行了编码:
'A' -> 1
'B' -> 2
...
'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
解答思路
这是一道动态规划的题目。我们可以用dp[i]表示字符串前i个字符的解码方法总数。那么对于第i个字符,如果它可以和前一个字符组成一个数字,则dp[i] = dp[i-1] + dp[i-2];否则,dp[i] = dp[i-1]。
具体来说,如果第i-1个字符为1,或者第i-1个字符为2且第i个字符小于等于6,则第i个字符可以和第i-1个字符组成一个数字,此时dp[i] = dp[i-1] + dp[i-2];否则,第i个字符不能和第i-1个字符组成一个数字,此时dp[i] = dp[i-1]。
最终的答案即为dp[n],其中n为字符串长度。
参考代码
下面是Java语言的实现代码:
public static int numDecodings(String s) {
int n = s.length();
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = s.charAt(0) == '0' ? 0 : 1;
for (int i = 2; i <= n; i++) {
int one = Integer.parseInt(s.substring(i - 1, i));
int two = Integer.parseInt(s.substring(i - 2, i));
if (one >= 1 && one <= 9) {
dp[i] += dp[i - 1];
}
if (two >= 10 && two <= 26) {
dp[i] += dp[i - 2];
}
}
return dp[n];
}
其中,s为输入的字符串,该方法返回解码方法的总数。
效率问题
在实际使用中,该算法的时间复杂度为O(n),其中n为字符串的长度,空间复杂度为O(n)。
在实际情况下,如果字符串长度较小,该算法的效率是可以接受的。但如果字符串长度非常大,可能会出现效率问题。此时,我们可以采取优化措施,比如使用滚动数组来减少空间占用,或者使用一些特殊技巧来加速计算过程。
另外,由于该算法是动态规划算法,所以在实际使用中还需要考虑一些细节问题,比如边界条件的处理、特殊情况的考虑等等。因此,在实际使用中需要仔细评估该算法的适用性和可行性。
两种加速计算过程的方法
滚动数组
由于dp[i]只与dp[i-1]和dp[i-2]有关,因此我们可以使用滚动数组来减少空间占用。具体来说,我们只需要用两个变量pre和cur来分别表示dp[i-2]和dp[i-1],然后根据递推公式计算出dp[i]即可。
下面是使用滚动数组实现的Java代码:
public static int numDecodings(String s) {
int n = s.length();
int pre = 1;
int cur = s.charAt(0) == '0' ? 0 : 1;
for (int i = 2; i <= n; i++) {
int one = Integer.parseInt(s.substring(i - 1, i));
int two = Integer.parseInt(s.substring(i - 2, i));
int tmp = cur;
if (one >= 1 && one <= 9) {
cur += pre;
}
if (two >= 10 && two <= 26) {
cur += pre;
}
pre = tmp;
}
return cur;
}
贪心算法
由于每个字符只能被解码成一个数字,因此我们可以从左到右依次扫描字符串,每次尽可能地解码出一个数字。具体来说,如果当前字符为1到9之间的数字,则直接解码出一个数字;如果当前字符为0,则只能和前一个字符组成一个数字;如果当前字符为大于9的数字,则只能和前一个字符组成一个数字。
下面是使用贪心算法实现的Java代码:
public static int numDecodings(String s) {
int n = s.length();
if (n == 0 || s.charAt(0) == '0') {
return 0;
}
int pre = 1;
int cur = 1;
for (int i = 1; i < n; i++) {
int tmp = cur;
if (s.charAt(i) == '0') {
if (s.charAt(i - 1) == '1' || s.charAt(i - 1) == '2') {
cur = pre;
} else {
return 0;
}
} else if (s.charAt(i - 1) == '1' || (s.charAt(i - 1) == '2' && s.charAt(i) >= '1' && s.charAt(i) <= '6')) {
cur += pre;
}
pre = tmp;
}
return cur;
}
这两种方法都能加速计算过程,但需要注意的是,贪心算法并不能保证得到最优解,因此在实际使用中需要仔细评估其适用性和可行性。
股票买卖问题
问题描述
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。 注意:你不能在买入股票前卖出股票。
解题思路
这是一道经典的股票买卖问题,可以使用贪心算法或动态规划算法来解决。
贪心算法
我们可以从左到右遍历整个数组,记录当前的最小值minPrice和最大利润maxProfit。对于每个元素prices[i],如果它小于minPrice,则更新minPrice为prices[i];否则,计算当前利润prices[i]-minPrice,并更新maxProfit为当前利润和maxProfit中的较大值。最终返回maxProfit即可。
下面是使用贪心算法实现的Java代码:
public static int maxProfit(int[] prices) {
int n = prices.length;
if (n == 0) {
return 0;
}
int minPrice = prices[0];
int maxProfit = 0;
for (int i = 1; i < n; i++) {
if (prices[i] < minPrice) {
minPrice = prices[i];
} else {
maxProfit = Math.max(maxProfit, prices[i] - minPrice);
}
}
return maxProfit;
}
动态规划算法
我们可以定义状态dp[i][0/1]表示第i天结束时,手中没有股票/手中持有一支股票时的最大利润。根据题目要求,最终的答案为dp[n-1][0],即最后一天手中没有股票时的最大利润。
对于第i天结束时手中没有股票的情况,有两种可能的转移:
- 前一天手中没有股票,今天不进行任何操作:dp[i][0] = dp[i-1][0]
- 前一天手中持有一支股票,今天卖出股票:dp[i][0] = dp[i-1][1] + prices[i]
对于第i天结束时手中持有一支股票的情况,同样有两种可能的转移:
- 前一天手中持有一支股票,今天不进行任何操作:dp[i][1] = dp[i-1][1]
- 前一天手中没有股票,今天买入一支股票:dp[i][1] = -prices[i]
最终的状态转移方程为:
math复制
dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])
下面是使用动态规划算法实现的Java代码:
public static int maxProfit(int[] prices) {
int n = prices.length;
if (n == 0) {
return 0;
}
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
return dp[n-1][0];
}
以上两种算法都可以得到正确的答案,但需要注意的是,贪心算法并不能保证得到最优解,因此在实际使用中需要仔细评估其适用性和可行性。
两种方法的比较
在时间复杂度上,贪心算法的时间复杂度为O(n),而动态规划算法的时间复杂度也为O(n),因此两种算法的时间复杂度是一致的。
在空间复杂度上,贪心算法只需要常数级别的空间,而动态规划算法需要一个二维数组来记录状态,因此空间复杂度为O(n)。因此,从空间复杂度上来看,贪心算法更优。
综合考虑,由于两种算法的时间复杂度相同,但贪心算法的空间复杂度更低,因此在解决这类问题时,我们可以优先考虑使用贪心算法。
数组足够大时可能的解决方案
如果数组足够大的话,可以使用分治算法来解决这个问题。具体来说,我们可以将数组分成两个部分,分别在左半部分和右半部分找到最大利润,然后再在跨越中点的子数组中找到最大利润。最终的答案即为这三个值中的最大值。
下面是使用分治算法实现的Java代码:
public static int maxProfit(int[] prices) {
return maxProfit(prices, 0, prices.length-1);
}
private static int maxProfit(int[] prices, int left, int right) {
if (left >= right) {
return 0;
}
int mid = left + (right - left) / 2;
int maxProfitLeft = maxProfit(prices, left, mid);
int maxProfitRight = maxProfit(prices, mid+1, right);
int minPrice = prices[left];
int maxProfitCross = 0;
for (int i = left+1; i <= right; i++) {
minPrice = Math.min(minPrice, prices[i]);
maxProfitCross = Math.max(maxProfitCross, prices[i] - minPrice);
}
return Math.max(Math.max(maxProfitLeft, maxProfitRight), maxProfitCross);
}
由于分治算法每次将数组分成两个部分,因此递归深度为O(logn),而每次计算跨越中点的最大利润需要O(n)时间,因此总时间复杂度为O(nlogn)。由于空间复杂度为O(logn),因此分治算法在空间复杂度上也比动态规划算法更优。但需要注意的是,分治算法的常数因子较大,因此在实际使用中需要权衡时间和空间的消耗。
参考
[1] 谷歌浏览器插件monica的回答
[2] 算法面试题参考自# 阿里面试算法题合集一