徒手挖地球二七周目
NO.60 第K个排列 中等
思路一:逆用康托展开 什么是康托展开式可以先从百度了解。本文直接从示例着手。
利用康托展开求排列是第几个,例如"213"是n=3的全排列中的第几个?[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZqMsYQkZ-1583742743249)(https://s2.ax1x.com/2020/03/08/3x5LgH.png)]
其实就是找出比"213"小的排列有几个,然后+1。
先比较首位,比2小的只有1,1开头的排列有1*2!个。
第二位比1小的不存在所以有0*1!排列。
第三位比3小的有1和2但是前面已经用过所以有0*0!个。
综上所述,有2个比"213"小的排列,所以"213"排在第3位。
本题是逆用,知道n和第k个,求排列本身。
例如:要找n=5,k=35的排列。共有5!=120个排列,从小到大要找第35个排列。
从首位开始找,首位分别是1、2、3、4、5的排列分成五组,每组各有(5-1)!=24个,由于k=35,所以要找的排列在第二组中,就是首位是2开头的24个排列中,即目标排列首位是"2"。
再看第二位,第二位可能是1、3、4、5分成四组,每组分别有(5-2)!=6个,由于k-24=11,所以要找的排列在第二组中,即目标排列的前两位是"23"。
同理,第三位可能是1、4、5分成三组,每组分别有(5-3)!=2个,因为k-6=5,所以要找的排列在第三组的两个排列中,即目标排列的前三位是"235"。
再然后第四位可能是1、4,分两组各有(5-4)!=1个,因为k-4=1,所以在第一组,即目标排列的前四位是"2351"。
最后第五位只剩下4,所以目标排列是"23514"。
找到第35个排列。
其实这个方法就是分组,上例中找第一位时候将有序全排列按照首位数字分为5组,看看我们要找的那个排列在第几组中从而确定了首位数字。
public String getPermutation(int n, int k) {
StringBuilder ans=new StringBuilder();
//所有可用数字
StringBuilder map=new StringBuilder();
for (int i = 1; i <= n ; i++)map.append(i);
int groupId,index=n;
//逐位确定
for (int i = 1; i <= n; i++) {
index--;
//确定在当前分组的第几组
groupId=(k-1)/foc(index);
//得到分组的数字,移除使用过的数字
ans.append(map.charAt(groupId));
map.deleteCharAt(groupId);
//更新k
k-=groupId*foc(index);
}
return ans.toString();
}
// 求 i!
private int foc(int i) {
int x=1;
while (i>=1){
x*=i--;
}
return x;
}
时间复杂度:O(n^2) 题目说n的范围[1,9],可以直接将1~9的阶乘预先保存在一个数组中。
public String getPermutation(int n, int k) {
StringBuilder ans=new StringBuilder();
//0~9的阶乘,直接使用
int factor[] = {1,1,2,6,24,120,720,5040,40320,362880};
//所有可用数字
StringBuilder map=new StringBuilder();
for (int i = 1; i <= n ; i++)map.append(i);
int groupId,index=n;
//逐位确定
for (int i = 1; i <= n; i++) {
index--;
//确定在当前分组的第几组
groupId=(k-1)/factor[index];
//得到分组的数字,移除使用过的数字
ans.append(map.charAt(groupId));
map.deleteCharAt(groupId);
//更新k
k-=groupId*factor[index];
}
return ans.toString();
}
时间复杂度:O(n)
NO.61 旋转链表 中等
思路一:成环再断 先将链表首尾相连形成环,然后找到旋转后的新链表的新头节点和尾节点,从两者之间断开形成需要的结果。
public ListNode rotateRight(ListNode head, int k) {
if(head==null||head.next==null)return head;
//标记初始链表的结尾,n记录节点数
ListNode oldTail=head;
int n=1;
while (oldTail.next!=null){
oldTail=oldTail.next;
n++;
}
//链表成环
oldTail.next=head;
//旋转之后的新结尾
ListNode newTail=head;
for (int i = 1; i < n - k % n; i++) {
newTail=newTail.next;
}
//新头节点
ListNode newHead=newTail.next;
//新尾节点和新头节点之间断开
newTail.next=null;
return newHead;
}
时间复杂度:O(n)
NO.121 买卖股票的最佳时机 简单
思路一:暴力法 没什么好说的,双重循环计算所有元素两两组合相减的结果,取最大。
public int maxProfit(int[] prices) {
int maxProfit=0;
for (int i = 0; i < prices.length - 1; i++) {
for (int j = i+1; j < prices.length; j++) {
maxProfit=Math.max(maxProfit,prices[j]-prices[i]);
}
}
return maxProfit;
}
时间复杂度:O(n^2)
思路二:优化暴力法到一次遍历 买卖股票从第二天开始我们每天都会"后悔":后悔没有在之前的最低点进行买入,只有这样我们的收益才会最大化。
由此可见,我们想要当天利益最大化,只需要在过去的某个最低点买入股票就好。所以我们只需要记录曾经出现过的最低点就好。
public int maxProfit(int[] prices) {
int minPoint=Integer.MAX_VALUE,maxProfit=0;
for (int i = 0; i < prices.length; i++) {
//记录曾出现过最低点
minPoint=Math.min(prices[i],minPoint);
//当日-曾经的最低
maxProfit=Math.max(maxProfit,prices[i]-minPoint);
}
return maxProfit;
}
时间复杂度:O(n)
从NO.121题不难看出:买股票的最佳时机是曾经!股市有风险,入股需谨慎!(狗头)
单纯的解答本题是比较简单的,但是买卖股票可以算作是一个系列的经典问题,在leetcode上就有本题一系列的变种问题:买卖股票的最佳时机、买卖股票的最佳时机II、买卖股票的最佳时机III、买卖股票的最佳时机IV、买卖股票的最佳时机含冷冻期、买卖股票的最佳时机含手续费。
虽然这些题有难有易,但是既然是一类问题,就有这一些通用的方法。
这六个问题都是由第四个问题简化变种而来的,第四题相较于本题多了一个参数k,限制只能进行k次交易;第一题也就是本题是只进行一次交易,相当于 k = 1;第二题是不限交易次数,相当于 k = +infinity(正无穷);第三题是只进行 2 次交易,相当于 k = 2;剩下两道也是不限次数,但是加了交易「冷冻期」和「手续费」的额外条件,其实就是第二题的变种。
之后记一下笔记总结一下这几个问题。
目前找到了"labuladong"大佬的公众号写的一篇《一个方法团灭 6 道股票问题》题解,打算先学习在总结。
本人菜鸟,有错误请告知,感激不尽!