面试题49:丑数
题目:我们把只包含因子2、3和5的数称作丑数(Ugly Number)。求按从小到大的顺序的第1500个丑数。例如6、8都是丑数,但14不是,因为它包含因子7。习惯上我们把1当做第一个丑数。
- 算法:暴力
- 丑数在一直除2一直除3一直除5后结果是1
public int nthUglyNumber(int n) { checkArguments(n); int number = 0; int uglyIndex = 0; while (uglyIndex < n) { number++; if (isUglyNumber(number)) { uglyIndex++; } } return number; } public boolean isUglyNumber(int n) { while ((n & 1) == 0) { n >>= 1; } while (n % 3 == 0) { n /= 3; } while (n % 5 == 0) { n /= 5; } return n == 1; }
- 算法:空间换时间
- 保存结果到数组中
- 下一个丑数一定是前面的丑数分别乘2乘3乘5中的最小但又大于目前最大丑数的值
- 遍历的同时分别找到乘2乘3乘5刚好大于目前最大丑数的位置
public int nthUglyNumber(int n) { checkArguments(n); int[] uglyArray = new int[n]; uglyArray[0] = 1; int multiply2Index = 0; int multiply3Index = 0; int multiply5Index = 0; for (int i = 1; i < n; i++) { uglyArray[i] = Math.min(Math.min(uglyArray[multiply2Index]*2, uglyArray[multiply3Index]*3), uglyArray[multiply5Index]*5); if (uglyArray[multiply2Index] * 2 == uglyArray[i]) { multiply2Index++; } if (uglyArray[multiply3Index] * 3 == uglyArray[i]) { multiply3Index++; } if (uglyArray[multiply5Index] * 5 == uglyArray[i]) { multiply5Index++; } } return uglyArray[n-1]; }
面试题50:第一个只出现一次的字符
题目一:字符串中第一个只出现一次的字符
题目:在字符串中找出第一个只出现一次的字符。如输入"abaccdeff",则输出'b'。
- 算法:基于数组创建简单的哈希表
- 键是ASCII码
- 值是字符出现的次数
public char firstUniqChar(String s) { checkArguments(s); int[] hashTable = new int[256]; for (char c : s.toCharArray()) { hashTable[c]++; } for (char c : s.toCharArray()) { if (hashTable[c] == 1) { return c; } } return ' '; }
题目二:字符流中第一个只出现一次的字符
题目:请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是'g'。当从该字符流中读出前六个字符"google"时,第一个只出现一次的字符是'l'。
class CharStatistics { // occurrence[ch] = 0: ch has not been found // occurrence[ch] >= 1: ch has been found one times and occurrence[ch] is the index+1 of ch in string stream // occurrence[ch] = -1: ch has been found for multiple times private int[] occurrence = new int[256]; private int index = 1; public void insert(char ch) { if (occurrence[ch] == 0) { occurrence[ch] = index++; } if (occurrence[ch] >= 1) { occurrence[ch] = -1; } } public char firstAppearingOnce() { char result = ' '; int minIndex = Integer.MAX_VALUE; // 因为字符流随时间增长可能变得很长,所以选择遍历数组而不是遍历当前的字符流。时间复杂度就是O(1) for (int i = 0; i < 256; i++) { if (occurrence[i] >= 1 && occurrence[i] < minIndex) { result = (char) i; minIndex = occurrence[i]; } } return result; } }
面试题51:数组中的逆序对
题目:在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
- 算法:归并排序
- nums数组和copy数组在递归时掉包了
- 每次在
left = reversePairs(copy, nums, start, start+middle);right = reversePairs(copy, nums, start+middle+1, end);
函数中对nums数组排序后,在reversePairs(nums, copy, start, middle, end)
函数中就可以利用左右有序计算逆序对 - 返回最后结果时,copy数组是有序的,nums数组是左右两边分别有序的
public int reversePairs(int[] nums) { checkArguments(nums); int[] copy = new int[nums.length]; System.arraycopy(nums, 0, copy, 0, nums.length); return reversePairs(nums, copy, 0, nums.length-1); } public int reversePairs(int[] nums, int[] copy, int start, int end) { if (start == end) { copy[start] = nums[start]; return 0; } else { int middle = (end - start) >> 1; int left = reversePairs(copy, nums, start, start+middle); int right = reversePairs(copy, nums, start+middle+1, end); return left + right + reversePairs(nums, copy, start, middle, end); } } public int reversePairs(int[] nums, int[] copy, int start, int middle, int end) { int i = start + middle; int j = end; int copyIndex = end; int count = 0; while (i >= start && j >= start+middle+1) { if (nums[i] > nums[j]) { copy[copyIndex--] = nums[i--]; count += (j - start - middle); } else { copy[copyIndex--] = nums[j--]; } } for (;i >= start; i--) { copy[copyIndex--] = nums[i]; } for (;j >= start+middle+1; j--) { copy[copyIndex--] = nums[j]; } return count; } public void checkArguments(int[] array) { if (array == null || array.length <= 1) { throw new RuntimeException("Invalid array"); } }
面试题52:两个链表的第一个公共结点
题目:输入两个链表,找出它们的第一个公共结点。
- 算法:
- LeetCode Discuss
- 使用两个指针进行遍历,当一个指针到头时,就重置它为另一个链表的头节点
- 当两个链表长度相同时:不到一轮遍历既能找到答案
- 当两个链表长度不同时:不到二轮遍历既能找到答案
public ListNode getIntersectionNode(ListNode l1, ListNode l2) { // checkArguments(l1, l2); // l1 == null; l2 == null; ListNode move1 = l1; ListNode move2 = l2; while (move1 != move2) { move1 = (move1 == null ? l2 : move1.next); move2 = (move2 == null ? l1 : move2.next); } return move1; }
- 算法:
- 首先遍历计算两个链表长度之差lengthDiff
- 然后长链表先走lengthDiff步
- 最后一起遍历直到找到公共结点
- O(m+n)
public ListNode getIntersectionNode(ListNode headA, ListNode headB) { checkArguments(headA); checkArguments(headB); int lengthA = getLengthOfList(headA); int lengthB = getLengthOfList(headB); int lengthDiff = lengthA > lengthB ? (lengthA - lengthB) : (lengthB - lengthA); ListNode longList = lengthA > lengthB ? headA : headB; ListNode shortList = lengthA > lengthB ? headB : headA; for (int i = 0; i < lengthDiff; i++) { longList = longList.next; } while (longList != null && shortList != null && (longList != shortList)) { longList = longList.next; shortList = shortList.next; } return longList; } public int getLengthOfList(ListNode head) { int length = 0; while (head != null) { length++; head = head.next; } return length; }
- 算法:
- 使用两个辅助栈把两个链表结点顺序入栈
- 这样出栈时就可以从尾部往头部遍历结点以此找到公共结点
public ListNode getIntersectionNode(ListNode headA, ListNode headB) { checkArguments(headA); checkArguments(headB); Stack<ListNode> stackA = new Stack<>(); Stack<ListNode> stackB = new Stack<>(); while (headA != null) { stackA.push(headA); headA = headA.next; } while (headB != null) { stackB.push(headB); headB = headB.next; } ListNode result = null; while (!stackA.isEmpty() && !stackB.isEmpty()) { ListNode nodeA = stackA.pop(); ListNode nodeB = stackB.pop(); if (nodeA != nodeB) { return result; } result = nodeA; } return result; }