(七)分析类算法
一、基本概念
对于有些问题可能没有特定的算法去解决,或者说需要我们仔细分析才能发现规律,但往往这种耗费精力多且收益差,这时最佳的姿势是通过将问题的手动解法变为自动化的形式。
二、问题分析
问题一:元素筛选
问题描述:每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1),如果没有小朋友,请返回-1
算法选型:
由于当前没有合适的算法可以直接选用,且暂时没分析出什么规律,因而只能通过模拟当前问题发生的过程求解。
采用上述设计技巧的算法逻辑:
/** 这道题不好直接解,可以用用模拟环的方式解决 **/ public class Solution { public int LastRemaining_Solution(int n, int m) { if(n==0 || m==0){ return -1; } // 刚开始所有人都没出,因而标记为0 int[] array = new int[n]; for(int i=0;i<n;i++){ array[i]=0; } int i = -1,step = 0, count = 0; while(true){ i++; // 越界的或者已经出现过了不允许计数 if(i>=n) i=0; if(array[i] == -1) continue; // 记录当前次数直到m step++; if(step==m) { array[i]=-1; step = 0; count++; if(count>=n-1){ break; } } } for(int k=0;k<array.length;k++){ if(array[k]==0){ return k; } } return -1; } }
问题二:环状入口点
问题描述:给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
分析与实现:
/** 分析如下,先给结论再分析: 1、设置快慢指针,假如有环,他们最后一定相遇(设置速度分别为2和1)。 2、两个指针分别从链表头和相遇点继续出发,每次走一步,最后一定相遇与环入口。 设:链表头到环入口长度为--a,环入口到相遇点长度为--b,相遇点到环入口长度为--c 则:相遇时,快指针路程=a+(b+c)k+b ,k>=1 其中b+c为环的长度,k为绕环的圈数(k>=1,即最少一圈,不能是0圈,不然和慢指针走的一样长,矛盾), 慢指针路程=a+b: 由于快指针走的路程是慢指针的两倍,所以:(a+b)*2=a+(b+c)k+b 化简可得:a=(k-1)(b+c)+c 这个式子的意思是: 链表头到环入口的距离=相遇点到环入口的距离+(k-1)圈环长度。其中k>=1,所以k-1>=0圈。所以两个指针分别从链表头和相遇点出发,最后一定相遇于环入口。 **/ /* public class ListNode { int val; ListNode next = null; ListNode(int val) { this.val = val; } } */ public class Solution { public ListNode EntryNodeOfLoop(ListNode pHead) { // 一快一慢找相遇点M ListNode slow = pHead; ListNode sQuick = pHead; ListNode meet1 = null; while(sQuick != null && sQuick.next != null){ slow = slow.next; sQuick = sQuick.next.next; if(slow==sQuick){ meet1=slow; break; } } // 如果相遇点为尾节点或者下一节点为null if(slow==null){ return null; } // 要保持环状,则相遇点的后一点需要有值 if(slow.next==null){ return null; } // 将其中一个重新放到初始点,另一个从M触发,以相同速度1结合以上分析可知必然相遇到入口点 slow = pHead; while(slow != null){ if(slow==sQuick){ meet1=slow; break; } slow = slow.next; sQuick = sQuick.next; } return meet1; } }
问题三:正则表达式有效性验证
问题描述:请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配。
问题分析:
(1)假设第二个字符是*时,则:
如果字符串第一个字符与模式串的第一个字符不匹配,则模式后移2个字符,继续匹配。
如果字符串第一个字符与模式串第一个字符匹配,那么有两种匹配模式:
- 模式串后移2个字符,相当于x*被忽略
- 字符串后移一个字符,模式不变,即继续匹配字符下一位,因为*可以匹配多位
(2)假设第二个字符不是*,则当前字符串的字符与模式串的当前字符必须匹配才能往下继续匹配
在上述匹配过程中,由于涉及到数组下标的变化,因此必须对数组和模式串的下标进行标记
代码实现:
/** 解题思路: 假设第二个字符是*时,则: 如果字符串第一个字符与模式串的第一个字符不匹配,则模式后移2个字符,继续匹配。 如果字符串第一个字符与模式串第一个字符匹配,那么有两种匹配模式: (1)模式串后移2个字符,相当于x*被忽略 (2)字符串后移一个字符,模式不变,即继续匹配字符下一位,因为*可以匹配多位 假设第二个字符不是*,则当前字符串的字符与模式串的当前字符必须匹配才能往下继续匹配 在上述匹配过程中,由于涉及到数组下标的变化,因此必须对数组和模式串的下标进行标记 **/ public class Solution { public boolean match(char[] str, char[] pattern) { if(str==null || pattern==null){ return false; } return isMatch(str, pattern, 0, 0); } boolean isMatch(char[]str, char[]pattern, int index, int indexPattern){ // 终态条件, 同时到尾巴必然返回true,模式串先到尾则返回false if(index == str.length && indexPattern == pattern.length){ return true; } if(index != str.length && indexPattern == pattern.length){ return false; } // 查看模式串的下一个字符是否为* if(indexPattern + 1 < pattern.length && pattern[indexPattern+1] == '*'){ if(index < str.length && (str[index]==pattern[indexPattern] || pattern[indexPattern]=='.')){ return isMatch(str, pattern, index, indexPattern+2) || isMatch(str, pattern, index+1, indexPattern); }else{ return isMatch(str, pattern, index, indexPattern+2); } }else{ if(index<str.length && (str[index]==pattern[indexPattern] || pattern[indexPattern]=='.')){ return isMatch(str, pattern, index+1, indexPattern+1); }else{ return false; } } } }
问题四:剪绳子问题
问题描述:给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],...,k[m]。请问k[0]xk[1]x...xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
问题分析:
(1)当n<=3时,通过枚举可知,最大值为n-1
(2)当n>3时需要对问题的最大值进行分析:
当k[0]==k[1]==..==k[m]时乘积最大,设k[0]=x,那么n=x*m,乘积可用右式表示:f(x)=(x)^(n/x)
其导数为:f'(x)=(n/(x^2)(1-lnx))x^(n/x),由其导数可知,其仅在(0,e)递增,考虑到只能取整数,那么x只能切分为2或者3达到最大,以n=6为例,如果都切分为2,则值为8,切分为3时值为9,因此应该切分为3,
总结:n>3时应保证以3为单位切分,且另一边值>1,即应在n>4的情况下才能保证,而当n==4时,通过枚举法可知最大值必然为4
代码实现:
/** n<=3时,max=n-1 n>4时,max=3*..z,其中z>1 */ public class Solution { public int cutRope(int target) { if(target<=3){ return target-1; } if(target==4){ return 4; } int max = 1; while(target>4){ max = max * 3; target = target - 3; } return target*max; } }