剑指offer学习笔记(Java实现)(26-30)
来源:互联网 发布:linux命令大全下载 编辑:程序博客网 时间:2024/05/17 09:03
题目26:复杂链表的复制。
定义一个新的数据结构,每个节点除了具有普通链表的next域外,还有一个额外的引用指向任意节点。我们要对由该特殊数据结构形成的链表进行复制。
我的方法:也就是克隆一个这种特殊链表,很快想到先不考虑原链表sibling域,复制出一个新的链表,然后再去给sibling域赋值。由于sibling可以指向任何节点,而且我们是根据原链表的sibling来确定新链表中的sibling,所以每次我们寻找新链表中某个节点的sibling,都要两个指针重新从头开始扫描以确定新链表中的sibling。所以时间复杂度是O(n)+O(n^2)。
public ComplexListNode complexClone(ComplexListNode head){ if(head == null)return null; ComplexListNode headMark = head; ComplexListNode newHeadMark = new ComplexListNode(head.val); ComplexListNode newHead = newHeadMark; head = head.next; //仅形成next域 for(; head != null; head = head.next){ newHead.next = new ComplexListNode(head.val); newHead = newHead.next; } //形成sibling域 head = headMark; newHead = newHeadMark; for(; head!=null; head=head.next, newHead=newHead.next){ ComplexListNode index = headMark; ComplexListNode newIndex = newHeadMark; for(; index!=null; index=index.next,newIndex=newIndex.next){ if(head.sibling == index){ newHead.sibling = newIndex; } } } return newHeadMark; }
书中方法一:一看书,果然上面的方法是最烂的,主要时间复杂度集中在第二步确定sibling上面,我们是否能够让每次寻找的时间复杂度减小到O(1)?一般来说牺牲空间复杂度可以提高时间复杂度+一次寻找的时间复杂度是O(1),我们就想到了HashMap。如何使用呢?我们在创建新链表的时候存储原链表每个节点对应的新链表节点,如(old,new),这样在第二步连接sibling的时候就可以根据原链表节点一步找到新链表节点。
public ComplexListNode complexClone2(ComplexListNode head){ if(head == null)return null; Map<ComplexListNode, ComplexListNode> map = new HashMap<>(); ComplexListNode headMark = head; ComplexListNode newHeadMark = new ComplexListNode(head.val); ComplexListNode newHead = newHeadMark; map.put(headMark, newHead); head = head.next; for(; head!=null; head = head.next){ newHead.next = new ComplexListNode(head.val); map.put(head, newHead.next); newHead = newHead.next; } for(ComplexListNode index = headMark,newIndex = newHeadMark; index!=null; index=index.next, newIndex=newIndex.next){ newIndex.sibling = map.get(index.sibling); } return newHeadMark; }
书中方法二:书上还讲了一种时间复杂度O(n)空间复杂度O(1)的方法。我们既可以根据原链表的节点迅速确定新链表的节点(即和HashMap一样也存在一种一一对应的关系),又不用额外创建空间。创建新链表时,把新链表的对应节点放在原链表节点的后面可以达到一一对应的效果,最后我们把一整条链表拆开,这样也不会破坏源链表结构,也得到了新链表。
public ComplexListNode complexClone3(ComplexListNode head){ copyAndConstruct(head); linkSibling(head); return unpackage(head); } private void copyAndConstruct(ComplexListNode head){ ComplexListNode index = head; while(index != null){ ComplexListNode temp = new ComplexListNode(index.val); temp.next = index.next; index.next = temp; index = index.next.next; } } private void linkSibling(ComplexListNode head){ ComplexListNode index = head; while(index != null){ if(index.sibling == null){ index.next.sibling = null; }else{ index.next.sibling = index.sibling.next; } index = index.next.next; } } private ComplexListNode unpackage(ComplexListNode head){ if(head == null)return null; ComplexListNode newIndex = head.next; ComplexListNode newHead = newIndex; ComplexListNode index = head; while(index != null){ index.next = newIndex.next; if(newIndex.next != null){ newIndex.next = newIndex.next.next; } index = index.next; newIndex = newIndex.next; } return newHead; }
题目27:二叉搜索树与双向链表
将BST改成排序的双向链表。
我的方法一:根据BST的性质,如果我们中序遍历BST,将会得到一个从小到大排序的序列。如果我们将包含这些数字的节点连接起来,就形成了一个链表,形成双向链表也很简单。关键是我们要知道我们在准备连接一个节点时,我们要知道它之前处理的那个节点,也就是小于它的最大一个节点。如果用迭代的方法,这个信息是丢失的,所以我们要用一个变量保存这个节点。下面是中序遍历的迭代方法,处理的过程变成了连接,处理完后更新lasthandle。
public TreeNode build(TreeNode root){ if(root == null)return null; TreeNode rootMark = root; Stack<TreeNode> stack = new Stack<>(); TreeNode lasthandle = null; while(root != null || stack.size()>0){ while(root != null){ stack.push(root); root = root.left; } if(stack.size()>0){ root = stack.pop(); //开始处理 root.left = lasthandle; if(lasthandle != null){ lasthandle.right = root; } //更新 lasthandle = root; root = root.right; } } //返回头节点 root = rootMark; while(root.left != null){ root = root.left; } return root; }
我的方法二:也可以采用递归的方法,我们利用递归函数的返回值返回处理好的双向链表的头节点,同时利用一个lastHandle变量保存上一次处理的节点(这个值会在递归函数中更新),这样我们就可以很方便地在递归的中序遍历中进行连接操作了。(但是一直没测试通过,应该是算法出了问题)
题目28:求字符串的全排列。
我的方法:求全排列,每一个字符都要用到且只能用一次,很自然的想到用一个boolean数组记录某个位置的字符是否用过,再用回溯的方法便可以得到全排列。缺点就是额外使用了O(n)的空间。
public List<String> permutation(String s){ List<String> result = new ArrayList<String>(); if(s == null)return result; //记录已经形成的串 String line = ""; //个人喜好 char[] c = s.toCharArray(); //记录某个位置上的字符是否使用过 boolean[] isUsed = new boolean[s.length()]; find(result, line, c, 0, isUsed); return result; } private void find(List<String> result, String lastInput, char[] c, int level, boolean[] isUsed){ //所有字符都使用过了 if(level == c.length){ result.add(new String(lastInput)); return; } //用于还原现场 String now = new String(lastInput); //每次从头开始找下一个没有使用过的字符 for(int i=0; i<c.length; i++){ if(!isUsed[i]){ lastInput += c[i]; isUsed[i] = true; find(result, lastInput, c, level+1, isUsed); isUsed[i] = false; lastInput = now; } } }
书中方法:同样也要保证每个数字只用一个,这里使用交换的方式,对于每一位,其后所剩字符表示没有确定的字符,都可以和该位交换(包括该位本身),交换完后该位确定,继续递归确定下一位,知道所有字符确定,则打印输出或保存。
public List<String> permutation2(String s){ List<String> result = new ArrayList<>(); if(s == null)return result; char[] c = s.toCharArray(); find2(result, c, 0); return result; } private void find2(List<String> result, char[] c, int start){ if(start >= c.length){ result.add(new String(c)); return; } //把start后的没有确定的元素都和当前位置的元素交换(包括当前位置的元素不动),以保证该位达到了“全排列”的效果。 //start代表当前需要确定的元素下标 for(int i=start; i<=c.length-1; i++){ exch(c, start, i); find2(result, c, start+1); exch(c, start, i); } }
题目28扩展1:求字符串的所有组合。
我的方法一:相比于上面的全排列(所有字符都要用到且只能使用一次),求所有组合的条件有所不同,即组合的大小不确定(1-n),字符同样是只能使用一次。如果我们再使用boolean数组来记录使用情况,每次都从头开始找当前位置的元素则可能产生重复的情况,例如“123”中长度为2的组合,先找了“12”,由2开始寻找的话就会找到“21”。我们只有不断向后寻找,才能避免重复。我们依旧用回溯,用一个下标start表示 选取输入字符串的从start开始的往后的各位添加进已有组合(也就是for循环),递归前就添加结果,而不是到了末尾或者某个标准才添加。例如输入”123”,我们得到的结果是1、12、123、13、2、23、3。
public List<String> combination(String s){ List<String> result = new ArrayList<String>(); if(s == null)return result; String line = ""; find(result,s, line, 0); return result; } private void find(List<String> result,String in, String line, int start){ if(start >= in.length())return; String now = new String(line); if(!line.equals("")){ result.add(new String(line)); } for(int i=start; i<=in.length()-1; i++){ line+=in.charAt(i); find(result, in, line, i+1); line = now; } }
我的方法二:上面的理解可能会有些难受,我们不如换一个理解方式,按照正常的思维,先找出长度为1的组合,然后长度为2的组合…..外层就是一个for循环。确定了组合的长度,剩下的问题就是在所有字符中选取m个字符(不能重复)。依旧要用到不断从剩下的数中选取数字的思想(其实这也是组合的定义)。那么这样“123”的查找就会变成1、2、3、12、13、23、123。
public List<String> combination2(String s){ List<String> result = new ArrayList<String>(); if(s == null)return result; String line = ""; //确定组合的长度 for(int i=1; i<=s.length(); i++){ find2(result, s, line, 0, 0, i); } return result; } private void find2(List<String> result,String in, String line, int start, int level, int border){ //组合长度到达要求才返回 if(level == border){ result.add(new String(line)); return; } String now = new String(line); for(int i=start; i<=in.length()-1; i++){ line += in.charAt(i); //在剩下的字符中继续寻找 find2(result, in, line, i+1, level+1, border); line = now; } }
书中方法:书中那段话的意思使用了组合的一个性质Cmn = Cm(n-1) + C(m-1)(n-1)或者是我理解错了,没想到怎么写,以后再说。
题目28扩展2:在8x8的棋盘上放置8个两两之间不能相互攻击的皇后(两两之间不在同一行、列、对角线)。
题目29:找出数组中出现次数超过一半的数字。
书中方法一:如果对数组进行排序,这个元素一定出现在中点上,这样做的时间复杂度是nlogn。也就是说第n/2大的数字一定是这种数字,现在我们不需要全部元素都排序这么强的条件,退一步,我们用快速排序中的partition方法将数组分割,如果分界点正好是n/2,那么就保证了这个分界点上的元素是第n/2大的元素。实质就是找第n/2大的数字,可以用分割的思想来做。下面用递归的方法对数组进行分割,直到n/2等于分界点。
public int findMajor(int[] a) throws Exception{ //如果数组为空,抛出异常 if(a == null || a.length == 0){ throw new IllegalArgumentException("输入待检查数组无效"); } //更改数组使分界点为中点 int mid = a.length>>1; find(a, 0, a.length-1, mid); //如果找到元素并不是major元素,抛出异常 int count = 0; for(int i=0; i<a.length; i++){ if(a[i] == a[mid]){ count++; } } if(count < (a.length>>1)){ throw new IllegalArgumentException("输入待检查数组不满足要求"); } return a[mid]; } private void find(int[] a, int start, int end, int target){ if(start>end)return; //随机找到一个当前分割点 int temp = partition(a, start, end); //如果分割点小于中点,对分割点右侧进行分割 if(temp < target)find(a, temp+1, end, target); //如果分割点大于中点,对分割点左侧进行分割 else if(temp>target)find(a, start, temp-1, target); } private int partition(int[] a, int start, int end){ //取第一个元素作为分割标志元素 int mark = a[start]; int left = start+1; int right = end; while(true){ while(left<=right && a[left]<mark){ left++; } while(left<=right && a[right]>=mark){ right--; } if(left == right+1){ break; } exch(a, left, right); } exch(a, start, right); return right; } private void exch(int[] a, int m, int n){ int temp = a[m]; a[m] = a[n]; a[n] = temp; } public static void main(String[] args){ int[] test = new int[]{1,2,3,4,4,4,4}; FindMajorNumInArray f = new FindMajorNumInArray(); try{ System.out.println(f.findMajor(test)); System.out.println(f.findMajor(new int[]{1})); //System.out.print(f.findMajor(new int[]{})); System.out.print(f.findMajor(new int[]{1,2,3,4,5})); }catch(Exception e){ e.printStackTrace(); } }
书中方法二:作为一个出现次数超过一半的主元素,除了排序后一定出现在中点上这个特性之外,还有一个特性就是出现次数大于其他元素出现次数综合,那么我们如何利用这个条件呢?我们访问的每一个元素都有可能是主元素,我们记录一个主元素和它的个数,如果下一个元素和它不同,它的个数减1,如果减到0了就说明它“暂时失去了主元素的地位”,我们更换主元素并把主元素个数清0。因为major元素的个数至少要比其他所有元素的个数多1,所以到最后选取出的暂时性的主元素一定就是最终的主元素。
public int findMajor2(int[] a) throws Exception{ //如果数组为空,抛出异常 if(a == null || a.length == 0){ throw new IllegalArgumentException("输入待检查数组无效"); } int lastMajor = a[0]; int accumulate = 1; for(int i=0; i<a.length; i++){ //如果上一个主元素并不存在“优势”了,更新主元素 if(accumulate == 0){ lastMajor = a[i]; accumulate = 1; }else if(a[i] == lastMajor){ accumulate++; }else if(a[i] != lastMajor){ accumulate--; } } //如果找到元素并不是major元素,抛出异常 int count = 0; for(int i=0; i<a.length; i++){ if(a[i] == lastMajor){ count++; } } if(count < (a.length>>1)){ throw new IllegalArgumentException("输入待检查数组不满足要求"); } return lastMajor; }
题目30:最小的K个数
找出输入数组中的最小的k个数。
书中方法一:利用分割数组的方法求第k大的数,还会带来一个附属效果——数组以这个数为界进行分割,左边的小于这个数,右边的大于等于这个数。如果我们按照求第k大的数的方法处理数组,那么取这个数左边的数字就可以得到最小的k个数。这种方法会改变原有数组。
public int[] find(int[] a, int k){ if(a.length == 0 || a == null || k<=0 || k > a.length){ return null; } int start = 0; int end = a.length-1; //以大小为标准,随机找一个元素(这里用的是第一个元素)分割数组,并返回分界点。 int mark = partition(a, start, end); while(mark != k){ if(mark < k){ start = mark+1; mark = partition(a, start, end); }else{ end = mark-1; mark = partition(a, start, end); } } int[] result = new int[k]; for(int i=0; i<k; i++){ result[i] = a[i]; } return result; } private int partition(int[] a, int start, int end){ //取第一个元素作为分割标志元素 int target = a[start]; int left = start+1; int right = end; while(true){ while(left<=right && a[left]<target){ left++; } while(left<=right && a[right]>=target){ right--; } if(left == right+1){ exch(a, right, start); return right; } exch(a, left, right); } } private void exch(int[] a, int m, int n){ int temp = a[m]; a[m] = a[n]; a[n] = temp; }
书中方法二:如果是海量数据,即数组很大,我们无法一次性将所有数据作为一个数组载入内存处理,这时候我们只需要在内存中开辟一个很小的空间用于存储当前最小的k个数,并不断从辅助存储空间读取需要处理的数据且不断更新最小的k个数。能用于对数据进行插入,查找,删除等操作并且速度较快的数据结构有很多,比如BST,堆,红黑树。我们设置容量上限为k。如果当前容量小于k,我们直接在数据结构中插入;如果当前容量等于k,我们进行判断:若要插入的数temp小于原数据结构中最大的数max,那么删除max,插入temp,若temp大于max,舍弃temp。由于实现这些数据结构比较麻烦,这里利用google的guava包里的TreeMultiSet来充当这个数据结构,该类既使用红黑树作为底层存储,而且允许多键共存,正好符合我们的要求。
public int[] find2(int[] a, int k){ if(a.length == 0 || a == null || k<=0 || k > a.length){ return null; } TreeMultiset<Integer> set = TreeMultiset.create(); for(int i=0; i<a.length; i++){ if(set.size()<k){ set.add(a[i]); }else{ if(a[i]<set.lastEntry().getElement()){ set.remove(set.lastEntry()); set.add(a[i]); } } } int[] result = new int[k]; int index = 0; Iterator<Integer> iterator = set.iterator(); while(index<k){ result[index] = iterator.next(); index++; } return result; }
扩展:BST、堆、红黑树的简单讨论
- 剑指offer学习笔记(Java实现)(26-30)
- 剑指offer学习笔记(Java实现)(11-20)
- 剑指offer学习笔记(Java实现)(1-10)
- 剑指offer学习笔记(Java实现)(21-25)
- 剑指offer学习笔记(Java实现)(31-40)
- 《剑指offer》笔记(java)
- (剑指offer)JAVA实现
- 剑指offer(Java实现)
- 剑指Offer学习笔记
- 《剑指offer》学习笔记
- Java笔记---剑指Offer(一:Java实现重建二叉树)
- 《剑指offer》 学习笔记(一)
- 剑指offer学习笔记(一)
- 剑指Offer学习笔记(2)
- 剑指Offer学习笔记(一)
- 剑指Offer笔记<JAVA版>(一)
- 剑指Offer笔记<JAVA版>(二)
- 剑指Offer笔记<JAVA版>(三)
- map容器
- 猜猜谁是我
- SCU4487 king's trouble I(深搜DFS)
- [省选] [树链剖分] [BZOJ2243] [SDOI2011] 染色
- struts2学习
- 剑指offer学习笔记(Java实现)(26-30)
- 网络同步赛——猜猜谁是我
- 初始容量问题
- 算年龄
- Unity OnBecameVisible 与 OnBecameInvisible 用法
- 大话设计模式之设计模式的六大原则
- Codeforces Round #358 (Div. 2) D. Alyona and Strings dp
- (1)算法学习-递归法
- 信息奥赛 回文数