• 第五章 优化时间和空间效率


    5.1 面试官谈效率
    • 时间、空间效率问题。
    • 面试一般要求空间和时间复杂度
    • 时间复杂度比较重要
     
    5.2 时间效率
    • 由于每个人都希望软件的响应时间尽量短一些,所以软件公司都很重视软件的时间性能,都会在发布软件之前花不少精力做时间效率优化。这也就不难理解为什么很多公司的面试官都把代码的时间效率当做一个考查重点。面试官除了考查应聘者的编程能力之外,还关注应聘者有没有不断优化效率、追求完美的态度和能力。
    • 同一个算法用循环和递归两种思路实现的时间效率可能会大不一样。递归的本质是把一个大的复杂问题分解成两个或者多个小的简单的问题。如果小问题中有相互重叠的部分,那么直接用递归实现虽然代码显得很简洁,但时间效率可能会非常差(详细讨论见本书2.4.2节)。对于这种类型的题目,我们可以用递归的思路来分析问题,但写代码的时候可以用数组(一维或者多维数组)来保存中间结果基于循环实现。绝大部分动态规划算法的分析和代码实现都是分这两个步骤完成的。
    • 代码的时间效率还能体现应聘者对数据结构和算法功底的掌握程度。同样是查找,如果是顺序查找需要O(m)的时间;如果输入的是排序的数组则只需要O(logm)的时间;如果事先已经构造好了哈希表,那查找在O(1)时间就能完成。我们只有对常见的数据结构和算法都了然于胸,才能在需要的时候选择合适的数据结构和算法来解决问题。
     
    面试题29:数组中出现次数超过一半的数字
    • 题目:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2, 3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。
    • 思路:将首次出现的数count+1,与之后的数进行比较,相等则+1,否则—1,count为0时换数,最后进行校验是否超过长度的一半。
      • 思路2:快排取中位数
    • 代码实现
      • public class TestMain {
      • public static void main(String[] args) {
      • TestMain t = new TestMain();
      • System.out.println(t.MoreThanHalfNum_Solution(new int[]{1,2,3,3,2,1,2,5,2,2,2}));
      • }
      • public int MoreThanHalfNum_Solution(int[] array) {
      • int maxCount = array[0];
      • int number = array[0];
      • int count = 1;
      • for (int i = 1; i < array.length; i++) {
      • if (number != array[i]) {
      • if (count == 0) {
      • number = array[i];
      • count = 1;
      • } else {
      • count--;
      • }
      • } else {
      • count++;
      • }
      • if (count == 1) {
      • maxCount = number;
      • }
      • }
      • // 验证
      • int num = 0;
      • for (int j = 0; j < array.length; j++) {
      • if (array[j] == maxCount) {
      • num++;
      • }
      • }
      • if (num * 2 > array.length) {
      • return maxCount;
      • }
      • return 0;
      • }
      • }
    • 测试用例:
      • 功能测试(输入的数组中存在-一个出现次数超过数组长度一半的数字,输入的数组中不存在一个出现次数超过数组长度-一半的数字)。
      • 特殊输入测试(输入的数组中只有一一个数字、输入NULL指针)。
    • 本题考点:
      • 考查对时间复杂度的理解。应聘者每想出一种解法,面试官都期待他能分析出这种解法的时间复杂度是多少。
      • 考查思维的全面性。面试官除了要求应聘者能对有效的输入返回正确的结果之外,同时也期待应聘者能对无效的输入作相应的处理。
     
    面试题30:最小的k个数
    • 题目:输入n个整数,找出其中最小的k个数。例如输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
    • 思路1:排序 取值
    • 思路2:额外数组
    • 思路3:堆排序
    • 代码实现
      • public class TestMain {
      • public static void main(String[] args) {
      • TestMain t = new TestMain();
      • System.out.println(t.GetLeastNumbers_Solution(new int[] { 1, 2, 3, 2, 5 },2));
      • }
      • public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
      • ArrayList<Integer> list = new ArrayList<>();
      • if (input == null || k <= 0 || k > input.length) {
      • return list;
      • }
      • int[] kArray = Arrays.copyOfRange(input, 0, k); // 创建大根堆
      • buildHeap(kArray);
      • for (int i = k; i < input.length; i++) {
      • if (input[i] < kArray[0]) {
      • kArray[0] = input[i];
      • maxHeap(kArray, 0);
      • }
      • }
      • for (int i = kArray.length - 1; i >= 0; i--) {
      • list.add(kArray[i]);
      • }
      • return list;
      • }
      • public void buildHeap(int[] input) {
      • for (int i = input.length / 2 - 1; i >= 0; i--) {
      • maxHeap(input, i);
      • }
      • }
      • private void maxHeap(int[] array, int i) {
      • int left = 2 * i + 1;
      • int right = left + 1;
      • int largest = 0;
      • if (left < array.length && array[left] > array[i])
      • largest = left;
      • else
      • largest = i;
      • if (right < array.length && array[right] > array[largest])
      • largest = right;
      • if (largest != i) {
      • int temp = array[i];
      • array[i] = array[largest];
      • array[largest] = temp;
      • maxHeap(array, largest);
      • }
      • }
      • }
    • 测试用例:
      • 功能测试(输入的数组中有相同的数字,输入的数组中没有相同的数字)。
      • 边界值测试(输入的k等于1或者等于数组的长度)
      • 特殊输入测试(k小于1、k大于数组的长度、指向数组的指针为NULL)。
    • 本题考点:
      • 考查对时间复杂度的分析能力。面试的时候每想出一个解法,我们都要能分析出这种解法的时间复杂度是多少。
      • 如果采用第一*种思路,本题考查对Partition 函数的理解。这个函数既是快速排序的基础,也可以用来查找n个数中第k大的数字。
      • 如果采用第二种,思路,本题考查对堆、红黑树等数据结构的理解。当需要在某数据容器内频繁查找及替换最大值时,我们要想到二叉树是个合适的选择,并能想到用堆或者红黑树等特殊的二叉树来实现。
     
    面试题31:连续子数组的最大和
    • 题目:输入一个整型数组,数组里有正数也有负数。数组中一个或连续的多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。
    • 思路:若和小于0,则将最大和置为当前值,否则计算最大和。
    • 代码实现
      • public class TestMain {
      • public static void main(String[] args) {
      • TestMain t = new TestMain();
      • System.out.println(t.FindGreatestSumOfSubArray(new int[] { 1, 2, 3, -2, 5,-2 }));
      • }
      • public int FindGreatestSumOfSubArray(int[] array) {
      • if (array == null || array.length == 0)
      • return 0;
      • int cur = array[0];
      • int greast = array[0];
      • for (int i = 1; i < array.length; i++) {
      • if (cur < 0) {
      • cur = array[i];
      • } else {
      • cur += array[i];
      • }
      • if (cur > greast) {
      • greast = cur;
      • }
      • }
      • return greast;
      • }
      • }
    • 测试用例:
      • 功能测试(输入的数组中有正数也有负数,输入的数组中全是正数,输入的数组中全是负数)。
      • 特殊输入测试(表示数组的指针为NULL指针)。
    • 本题考点:
      • 考查对时间复杂度的理解。这道题如果应聘者给出时间复杂度为O(n3)甚至O(n)的算法,是不能通过面试的。
      • 考查对动态规划的理解。如果应聘者熟练掌握了动态规划算法,那么他就能轻松地找到解题方案。如果没有想到用动态规划的思想,那么应聘者就需要仔细地分析累加子数组的和的过程,从而找到解题的规律。
      • 考查思维的全面性。能否合理地处理无效的输入,对面试结果有很重要的影响。
     
    面试题32:从1到n整数中1出现的次数
    • 题目:输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。例如输入12,从1到12这些整数中包含1的数字有1,10,11和12,1一共出现了5次。
    • 思路:思路:若百位上数字为0,百位上可能出现1的次数由更高位决定;若百位上数字为1,百位上可能出现1的次数不仅受更高位影响还受低位影响;若百位上数字大于1,则百位上出现1的情况仅由更高位决定
    • 代码实现
      • public class TestMain {
      • public static void main(String[] args) {
      • TestMain t = new TestMain();
      • System.out.println(t.CountOne2(100));
      • }
      • public long CountOne2(long n) {
      • long count = 0; // 1的个数
      • long i = 1; // 当前位
      • long current = 0, after = 0, before = 0;
      • while ((n / i) != 0) {
      • before = n / (i * 10);// 高位
      • current = (n / i) % 10;// 当前位
      • after = n - (n / i) * i; // 低位
      • if (current == 0) // 如果为0,出现1的次数由高位决定,等于高位数字 * 当前位数
      • count = count + before * i;
      • else if (current == 1) // 如果为1,出现1的次数由高位和低位决定,高位*当前位+低位+1
      • count = count + before * i + after + 1;
      • else if (current > 1)
      • // 如果大于1,出现1的次数由高位决定,(高位数字+1)* 当前位数
      • count = count + (before + 1) * i;
      • // 前移一位
      • i = i * 10;
      • }
      • return count;
      • }
      • }
    • 思路2:公式法
    • 实现代码
      • public class TestMain {
      • public static void main(String[] args) {
      • TestMain t = new TestMain();
      • System.out.println(t.NumberOf1Between1AndN_Solution(100));
      • }
      • public int NumberOf1Between1AndN_Solution(int n) {
      • int count = 0;
      • for (int i = 1; i <= n; i *= 10) {
      • int a = n / i;// 高位
      • int b = n % i;// 低位
      • count += (a + 8) / 10 * i;
      • if (a % 10 == 1) {
      • count += b + 1;
      • }
      • }
      • return count;
      • }
      • }
    • 测试用例:
      • 功能测试(输入5、10、55、99等)。
      • 边界值测试(输入0、1等)。
      • 性能测试(输入较大的数字如10000、21235 等)。
    • 本题考点:
      • 考查应聘者做优化的激情和能力。最原始的方法大部分应聘者都能想到。当面试官提示还有更快的方法之后,应聘者千万不要轻易放弃尝试。虽然想出O(logn)的方法不容易,但应聘者要展示自己追求更快算法的激情,多尝试不同的方法,必要的时候可以要求面试官给出提示,但不能轻易说自己想不出来并且放弃努力。
      • 考查面应聘者对复杂问题的思维能力。要想找到O(logn)的方法,应聘者需要有很严密的数学思维能力,并且还要通过分析具体例子一步步找到通用的规律。这些能力在实际工作中面对复杂问题的时候都非常有用。
     
    面试题33:把数组排成最小的数
    • 题目:输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32, 321},则打印出这3个数字能排成的最小数字321323。
    • 思路:先将整型数组转换成String数组,然后将String数组排序,最后将排好序的字符串数组拼接出来。关键就 是制定排序规则。或使用比较和快排的思想,将前面的数和最后的数比较,若小则放到最前面,最后再递归调用。
    • 代码实现
      • public class TestMain {
      • public static void main(String[] args) {
      • TestMain t = new TestMain();
      • System.out.println(t.PrintMinNumber(new int[]{3,32,321}));
      • }
      • public String PrintMinNumber(int[] numbers) {
      • if (numbers == null || numbers.length == 0)
      • return "";
      • int len = numbers.length;
      • String[] str = new String[len];
      • StringBuilder sb = new StringBuilder();
      • for (int i = 0; i < len; i++) {
      • str[i] = String.valueOf(numbers[i]);
      • }
      • Arrays.sort(str, new Comparator<String>() {
      • public int compare(String s1, String s2) {
      • String c1 = s1 + s2;
      • String c2 = s2 + s1;
      • return c1.compareTo(c2);
      • }
      • });
      • for (int i = 0; i < len; i++) {
      • sb.append(str[i]);
      • }
      • return sb.toString();
      • }
      • }
    • 测试用例:
      • 功能测试(输入的数组中有多个数字,输入的数组中的数字有重复的数位,输入的数字只有一个数字)。
      • 特殊输入测试(表示数组的指针为NULL指针)。
    • 本题考点:
      • 本题有两个难点;第一个难点是想出一~种新的比较规则来排序一个数组;第二个难点在于证明这个比较规则是有效的,并且证明根据这个规则排序之后把数组中所有数字拼接起来得到的数字是最小的。要想解决这两个难点,都要求应聘者有很强的数学功底和逻辑思维能力。
      • 考查解决大数问题的能力。应聘者在面试的时候要意识到,把两个int型的整数拼接起来得到的数字可能会超出int型数字能够表达的范围,从而导致数字溢出。我们可以用字符串表示数字,这样就能简洁地解决大数问题。
     
    5.3 时间效率与空间效率的平衡
    • 硬件的发展一直遵循着摩尔定律,内存的容量基本上每隔18个月就会翻一番。由于内存的容量增加迅速,在软件开发的过程中我们允许以牺牲一定的空间为代码来优化时间性能,以尽可能地缩短软件的响应时间。这就是我们通常所说的“以空间换时间”。
     
    面试题34:丑数
    • 题目:我们把只包含因子2、3和5的数称作丑数(Ugly Number)。求按从小到大的顺序的第1500个丑数。例如6、8都是丑数,但14不是,因为它包含因子7。习惯上我们把1当做第一个丑数。
    • 思路:乘2或3或5,之后比较取最小值。
    • 代码实现
      • public class TestMain {
      • public static void main(String[] args) {
      • TestMain t = new TestMain();
      • int[] arr = t.GetUglyNumber_Solution(10);
      • for(int i=0;i<arr.length;i++){
      • System.out.println(arr[i]);
      • }
      • }
      • public int[] GetUglyNumber_Solution(int index) {
      • if (index <= 0)
      • return null;
      • int[] arr = new int[index];
      • arr[0] = 1;
      • int multiply2 = 0;
      • int multiply3 = 0;
      • int multiply5 = 0;
      • for (int i = 1; i < index; i++) {
      • int min = Math.min(arr[multiply2] * 2, Math.min(arr[multiply3] * 3, arr[multiply5] * 5));
      • arr[i] = min;
      • if (arr[multiply2] * 2 == min)
      • multiply2++;
      • if (arr[multiply3] * 3 == min)
      • multiply3++;
      • if (arr[multiply5] * 5 == min)
      • multiply5++;
      • }
      • return arr;
      • }
      • }
    • 测试用例:
      • 功能测试(输入2、3、4、5、6等)。
      • 特殊输入测试(边界值1、无效输入0)。
      • 性能测试(输入较大的数字,如1500)。
    • 本题考点:
      • 考查应聘者对时间复杂度的理解。绝大部分应聘者都能想出第一种思路。在面试官提示还有更快的解法之后,应聘者能否分析出时间效率的瓶颈,并找出解决方案,是能否通过这轮面试的关键。
      • 考查应聘者的学习能力和沟通能力。丑数对很多人而言是个新概念。有些面试官喜欢在面试的时候定义一个新概念,然后针对这个新概念出面试题。这就要求应聘者听到不熟悉的概念之后,要有主动积极的态度,大胆向面试官提问,经过几次思考、提问、再思考的循环,在短时间内理解这个新概念。这个过程就体现了应聘者的学习能力和沟通能力。
     
    面试题35:第一个只出现一次的字符
    • 题目:在字符串中找出第一个只出现一次的字符。如输入"abaccdeff",则输出'b'。
    • 思路:利用LinkedHashMap保存字符和出现次数。
    • 代码实现
      • public class TestMain {
      • public static void main(String[] args) {
      • TestMain t = new TestMain();
      • System.out.println(t.FirstNotRepeatingChar("asdfsad"));
      • }
      • public int FirstNotRepeatingChar(String str) {
      • if (str == null || str.length() == 0)
      • return -1;
      • char[] c = str.toCharArray();
      • LinkedHashMap<Character, Integer> hash = new LinkedHashMap<Character, Integer>();
      • for (char item : c) {
      • if (hash.containsKey(item))
      • hash.put(item, hash.get(item) + 1);
      • else
      • hash.put(item, 1);
      • }
      • for (int i = 0; i < str.length(); i++) {
      • if (hash.get(str.charAt(i)) == 1) {
      • return i;
      • }
      • }
      • return -1;
      • }
      • }
    • 测试用例:
      • 功能测试(字符串中存在只出现一- 次的字符,字符串中不存在只出现一次字符,字符串中所有字符都只出现一次)。
      • 特殊输入测试(字符串为NULL指针)。
    • 本题考点:
      • 考查对数组、字符串的编程能力。
      • 考查对哈希表的理解及运用。
      • 考查对时间效率及空间效率的分析能力。当面试官提示最直观的算法不是最优解的时候,应聘者需要立即分析出这种算法的时间效率。在想出基于哈希表的算法之后,应聘者也应该分析出该方法的时间效率和空间效率分别是O(m)和O(1)。
    • 举一反三:
      • 如果需要判断多个字符是不是在某个字符串里出现过或者统计多个字符在某个字符串中出现的次数,我们可以考虑基于数组创建一个简单的哈希表。这样可以用很小的空间消耗换来时间效率的提升。
     
    面试题36:数组中的逆序对
    • 题目:在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
    • 思路:本质是归并排序,在比较时加入全局变量count进行记录逆序对的个数,若data[start] >= data[index] , 则count值为mid+1-start
    • 实现代码
      • public class TestMain {
      • public int count = 0;
      • public static void main(String[] args) {
      • TestMain t = new TestMain();
      • t.InversePairs(new int[] { 7, 5, 6, 4 });
      • System.out.println(t.count);
      • }
      • public int InversePairs(int[] array) {
      • if (array == null)
      • return 0;
      • mergeSort(array, 0, array.length - 1);
      • return count;
      • }
      • private void mergeSort(int[] data, int start, int end) {
      • int mid = (start + end) / 2;
      • if (start < end) {
      • mergeSort(data, start, mid);
      • mergeSort(data, mid + 1, end);
      • merge(data, start, mid, end);
      • }
      • }
      • public void merge(int[] data, int start, int mid, int end) {
      • int arr[] = new int[end - start + 1];
      • int c = 0;
      • int s = start;
      • int index = mid + 1;
      • while (start <= mid && index <= end) {
      • if (data[start] < data[index]) {
      • arr[c++] = data[start++];
      • } else {
      • arr[c++] = data[index++];
      • count += mid + 1 - start;
      • //count %= 1000000007;
      • }
      • }
      • while (start <= mid) {
      • arr[c++] = data[start++];
      • }
      • while (index <= end) {
      • arr[c++] = data[index++];
      • }
      • for (int d : arr) {
      • data[s++] = d;
      • }
      • }
      • }
    • 测试用例:
      • 功能测试(输入未经排序的数组、递增排序的数组、递减排序的数组,输入的数组中包含重复的数字)。
      • 边界值测试(输入的数组中只有两个数字、数组的数组只有一个数字)
      • 特殊输入测试(表示数组的指针为NULL指针)。
    • 本题考点:
      • 考查分析复杂问题的能力。统计逆序对的过程很复杂,如何发现逆序对的规律,是应聘者解决这个题目的关键。
      • 考查应聘者对归并排序的掌握程度。如果应聘者在分析统计逆序对的过程中发现问题与归并排序的相似性,并能基于归并排序形成解题思路,那通过这轮面试的几率就很高了。
     
    面试题37:两个链表的第一个公共结点
    • 题目:输入两个链表,找出它们的第一个公共结点。
    • 思路:先求出链表长度,然后长的链表先走多出的几步,然后两个链表同时向下走去寻找相同的节点,代码量少的方法需要将两个链表遍历两次,然后从头开始相同的节点。
      • // 不需要遍历链表的解法
      • public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
      • ListNode p1 = pHead1;
      • ListNode p2 = pHead2;
      • while (p1 != p2) {
      • p1 = (p1 != null ? p1.nextNode : pHead2);
      • p2 = (p2 != null ? p2.nextNode : pHead1);
      • }
      • return p1;
      • }
    • 测试用例:
      • 功能测试(输入的两个链表有公共交点:第一个公共结点在链表的中间,第一个公共结点在链表的末尾,第一个公共结点是链表的头结点;输入的两个链表没有公共结点)。
      • 特殊输入测试(输入的链表头结点是NULL指针)
    • 本题考点:
      • 考查应聘者对时间复杂度和空间复杂度的理解及分析能力。解决这道题有多种不同的思路。每当应聘者想到一种思路的时候,都要很快分析出这种思路的时间复杂度和空间复杂度是多少,并找到可以优化的地方。
      • 考查应聘者对链表的编程能力。
        
    5.4 本章小结
    • 降低时间复杂度方式
      • 优化算法
      • 使用辅助空间
     
     
     
     
     
     
     
     
     
     
     
     
     
  • 相关阅读:
    WHU 1540 Fibonacci 递推
    CSU 1378 Shipura 简单模拟
    UVALive 6486 Skyscrapers 简单动态规划
    JAVA杂记
    JAVA的main方法
    Java中的System类
    认识理解Java中native方法(本地方法)
    JAVA导入支持类
    从UDP的”连接性”说起–告知你不为人知的UDP
    udp如何实现可靠性传输?
  • 原文地址:https://www.cnblogs.com/Sungc/p/9333924.html
Copyright © 2020-2023  润新知