美文网首页
算法 | 一周刷完《剑指Offer》 Day3:第27~37题

算法 | 一周刷完《剑指Offer》 Day3:第27~37题

作者: 机盐Johnny | 来源:发表于2018-12-12 23:25 被阅读0次

    写在前面

    上一篇:算法 | 一周刷完《剑指Offer》 Day2:第17~26题
    下一篇:算法 | 一周刷完《剑指Offer》 Day4:第38~49题


    Day3:第27~37题

    难度上升,多看多想,理解才好做。

    • T27. 字符串的排列
    • T28. 数组中出现次数超过一半的数字
    • T29. 最小的K个数
    • T30. 连续子数组的最大和
    • T31. 整数中1出现的次数(从1到n整数中1出现的次数)
    • T32. 把数组排成最小的数
    • T33. 丑数
    • T34. 第一个只出现一次的字符位置
    • T35. 数组中的逆序对
    • T36. 两个链表的第一个公共结点
    • T37. 数字在排序数组中出现的次数

    T27. 字符串的排列

    题目描述

    输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

    输入描述:输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。

    解题思路

    同样是递归回溯的思想。先把字符串进行字典序排序,定义hasUsed辅助数组记录各字符是否使用,然后递归对后面的字符排列组合即可。

    注意:使用StringBuffer便于字符串操作。每个递归结束后记得回溯,去除此循环加入的字符,回退到上一步的排列,与T24中去除结点道理一样。

        private ArrayList<String> result = new ArrayList<>();
        
        public ArrayList<String> Permutation(String str) {
            if(str == null || str.length() == 0) return result;
            
            char[] chars = str.toCharArray();
            Arrays.sort(chars);//字典序排序
            
            permutation(chars,
                    new boolean[chars.length],//用于记录当前字符是否用过
                    new StringBuffer());//字符串,便于操作
            
            return result;
        }
        
        private void permutation(char[] chars, boolean[] hasUsed, StringBuffer str) {
            if(str.length() == chars.length) {//长度相同说明出结果,加入result
                result.add(str.toString());
                return;
            }
            
            for(int i = 0; i < chars.length; i++) {
                if(hasUsed[i]) continue;
                if(i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) continue;//连续两个值相同时,保证不重复
                hasUsed[i] = true;
                str.append(chars[i]);
                
                //递归对后面的字符进行排列
                permutation(chars, hasUsed, str);
                
                //此步重要,去除此循环加入的字符,回退到上一步的排列,与T24中去除结点道理一样
                str.deleteCharAt(str.length() - 1);
                hasUsed[i] = false;
                
            }
        }
    

    T28. 数组中出现次数超过一半的数字

    题目描述

    数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

    解题思路

    多数投票问题。

    首先明确,若数字出现次数超过一半,那它必为出现最多的数字。因此问题转换为找出现最多的数字,然后判断它出现的次数是否超过一半。

    定义count来统计一个元素出现的次数,当遍历到的元素和统计元素不相等时,count --。如果前面查找了 i 个元素,且 count == 0 ,说明前 i 个元素没有【多数】,或者有【多数】但出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 count 就一定不会为 0 。此时剩下的 n - i 个元素中,【多数】的数目依然多于 (n - i) / 2,因此继续查找就能找出【多数】。

    最后,找到多数后再判断出现次数是否超过一半即可。

        public int MoreThanHalfNum_Solution(int[] array) {//多数投票问题
            int num = array[0];
            int count = 1;
            
            for(int i = 1; i < array.length; i ++) {
                if(array[i] == num) {
                    count ++;
                } else {
                    count --;
                }
                if(count == 0) {
                    num = array[i];
                    count = 1;
                }
            }
            
            count = 0;
            for(int val: array) {
                if(val == num) {
                    count ++;
                }
            }
            
            return count > array.length / 2 ? num : 0;//三元
        }
    

    T29. 最小的K个数

    题目描述

    输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

    解题思路

    快速选择法。快速选择的总体思路与快速排序一致,选择一个元素作为基准来对元素进行分区,将小于和大于基准的元素分在基准左边和右边的两个区域。不同的是,快速选择并不递归访问双边,而是只递归进入一边的元素中继续寻找。

    快排的 partition() 方法会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素。

        public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
            ArrayList<Integer> list = new ArrayList<>();
            
            if(k > input.length || k <= 0) return list;
            
            int smallestK = findSmallestK(input, k - 1);
            
            for(int val: input) {
                if(val <= smallestK && list.size() < k) {
                    list.add(val);
                }
            }
            return list;
        }
        
        private int findSmallestK(int[] input, int k) {
            int low = 0;
            int high = input.length - 1;
            while(low < high) {
                int j = partition(input, low, high);
                if(j < k) {
                    low = j + 1;
                } else if(j > k) {
                    high = j - 1;
                } else {
                    break;
                }
            }
            return input[k];
        }
        
        private int partition(int[] nums, int low, int high) {
            int i = low;
            int j = high + 1;
            while(true) {
                while(i < high && nums[++ i] < nums[low]) ;
                while(j > low && nums[low] < nums[-- j]) ;
                if(i >= j) {
                    break;
                }
                swap(nums, i, j);
            }
            swap(nums, low, j);
            return j;
        }
        
        private void swap(int[] nums, int i, int j) {
            int tmp = nums[i];
            nums[i] = nums[j];
            nums[j] = tmp;
        }
    

    或者,直接排序找最小。。。

        public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
            ArrayList<Integer> list = new ArrayList<>();
            
            if(k > input.length || k <= 0) return list;
            
            Arrays.sort(input);
            for(int i = 0; i < k; i ++) {
                list.add(input[i]);
            }
            
            return list;
        }
    

    T30. 连续子数组的最大和

    题目描述

    HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

    解题思路

    嗯。。。阅读题,历来都是题目越长题越简单。。。单纯一点,边找边加就行了。

    注意查看代码中唯一的一行注释,很关键。

        public int FindGreatestSumOfSubArray(int[] array) {
            if(array == null || array.length == 0) return 0;
            
            int sum = 0;
            int result = Integer.MIN_VALUE;
            
            for(int val: array) {
                if(sum < 0) {
                    sum = val;//关键在此,如果前面n个的和sum已经小于0了,别傻乎乎继续加,直接从新的val开始吧
                } else {
                    sum += val;
                }
                
                if(result < sum) {
                    result = sum;
                }
            }
            
            return result;
        }
    

    T31. 整数中1出现的次数(从1到n整数中1出现的次数)

    题目描述

    求出1 ~ 13的整数中1出现的次数,并算出100 ~ 1300?的整数中1出现的次数?为此他特别数了一下1 ~ 13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

    解题思路

    这种题靠悟性。。。

        public int NumberOf1Between1AndN_Solution(int n) {
            int ones = 0;
            
            for(int m = 1; m <= n; m *= 10) {
                int a = n / m, b = n % m;
                if(a % 10 == 0)
                    ones += a / 10 * m;
                else if(a % 10 == 1) 
                    ones += (a / 10 * m) + (b + 1);
                else
                    ones += (a / 10 + 1) * m;
            }
            
            return ones;
        }
    

    leetcode大神只用了5行的解法,有兴趣的深入了解一下。。。
    https://leetcode.com/problems/number-of-digit-one/discuss/64381/4-lines-olog-n-cjavapython

        public int countDigitOne(int n) {
            int ones = 0;
            for (long m = 1; m <= n; m *= 10)
                ones += (n/m + 8) / 10 * m + (n/m % 10 == 1 ? n%m + 1 : 0);
            return ones;
        }
    

    T32. 把数组排成最小的数

    题目描述

    输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

    解题思路

    可以看做是排序问题,不同点在于此题是比较数字转换成字符串后相加的大小

    例如两个数字转换的字符串S1和S2,应该比较 S1+S2S2+S1 的大小,如果 S1+S2 < S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。

        public String PrintMinNumber(int[] numbers) {
            String[] nums = new String[numbers.length];
            for(int i = 0; i < nums.length; i ++) {//int转string,比较string相加的值
                nums[i] = String.valueOf(numbers[i]);
            }
            
            Arrays.sort(nums, (s1, s2) -> (s1 + s2).compareTo(s2 + s1));//排序,s1+s2与s2+s1两个字符串比较,谁小谁放前面
            String result = "";
            for(String str: nums) {
                result += str;
            }
            
            return result;
        }
    

    T33. 丑数

    题目描述

    把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

    解题思路

    此题需要思维灵活。由题意,只需不断从前面已知的丑数中选取合适的丑数分别乘2、3、5,选取最小的丑数加入数组即可。关键在于如何选取合适的丑数。(定义2、3、5对应的下标index2、index3、index5,详见注释)

        public int GetUglyNumber_Solution(int index) {
            if(index <= 6) return index;//1~6即为前6个丑数
            
            int index2 = 0, index3 = 0, index5 = 0;
            
            int[] uglys = new int[index];//存前n个丑数
            uglys[0] = 1;//初始化第一个值为1
            int n = 1;//开始计算第二个丑数
            
            while(n < index) {
                //找出下一个小的丑数,此步重要需理解,分别用2,3,5在丑数数组里对应的上一个丑数乘2,3,5找出最小的丑数
                int ugly2 = uglys[index2] * 2;
                int ugly3 = uglys[index3] * 3;
                int ugly5 = uglys[index5] * 5;
                int min = Math.min(ugly2, Math.min(ugly3, ugly5));
                
                uglys[n] = min;
                n ++;
                
                //将2,3,5对应的上一个丑数后移
                if(min == ugly2) index2 ++;
                if(min == ugly3) index3 ++;
                if(min == ugly5) index5 ++;
            }
            
            return uglys[index - 1];
        }
    

    T34. 第一个只出现一次的字符位置

    题目描述

    在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置,如果没有则返回 -1(需要区分大小写)。

    解题思路

    char类型一般为一个字节,范围在0 ~ 255。因此定义一个整形计数数组int[256],对每个char出现次数进行计数即可。

    计数后要按照字符串中的字符顺序查找第一个计数次数为1的字符。

        public int FirstNotRepeatingChar(String str) {
            int[] array = new int[256];//计数数组
            
            for(int i = 0; i < str.length(); i ++) {
                array[str.charAt(i)] ++;
            }
            
            for(int i = 0; i < str.length(); i ++) {
                if(array[str.charAt(i)] == 1) {//按str的字符顺序来,找出第一个计数次数为1的即为所求位置
                    return i;
                }
            }
            
            return -1;
        }
    

    T35. 数组中的逆序对

    题目描述

    在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007。

    解题思路

    分治思想,先分后治。先不断将数组一分为二,并对这分开的两部分进行相同操作;然后一边合并相邻的子数组,一边统计逆序对的数目。(实质就是归并排序的思路)

        private long cnt = 0;
        private int[] tmp;//辅助数组
        
        public int InversePairs(int[] array) {
            tmp = new int[array.length];
            
            mergeSortUp2Down(array, 0, array.length - 1);
            
            return (int) (cnt % 1000000007);
        }
        
        private void mergeSortUp2Down(int[] nums, int first, int last) {
            if(last - first < 1) return;
            
            int mid = (first + last) / 2;
            //分治思想
            mergeSortUp2Down(nums, first, mid);
            mergeSortUp2Down(nums, mid + 1, last);
            merge(nums, first, mid ,last);
        }
        
        private void merge(int[] nums, int first, int mid, int last) {
            int i = first, j = mid + 1, k = first;
            
            while(i <= mid || j <= last) {
                if(i > mid) {
                    tmp[k] = nums[j];
                    j ++;
                }
                else if(j > last) {
                    tmp[k] = nums[i];
                    i ++;
                }
                else if(nums[i] < nums[j]) {
                    tmp[k] = nums[i];
                    i ++;
                }
                else {
                    tmp[k] = nums[j];
                    j ++;
                    this.cnt += mid - i + 1;//nums[i] > nums[j]说明nums[i...mid]都大于nums[j]
                }
                k ++;
            }
            for(k = first; k <= last; k ++) {
                nums[k] = tmp[k];
            }
        }
    

    T36. 两个链表的第一个公共结点

    题目描述

    输入两个链表,找出它们的第一个公共结点。

    解题思路

    数学问题。

    如图,链表1长度为 a+c,链表2长度为 b+c。声明两个指针node1和node2分别指向两个链表表头,同步向后移动。

    node1走过 a+c 后指空,此时让它指向链表2的表头并继续向后走;同理node2走过 b+c 后指向链表1表头。

    由于 a+c+b = b+c+a ,此时node1和node2刚好相遇,且相遇在两个链表的第一个公共结点。由此得解。

        public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
            ListNode node1 = pHead1;
            ListNode node2 = pHead2;
            
            while(node1 != node2) {//公共结点后面即为公共链表
                if(node1 == null) {
                    node1 = pHead2;
                } else {
                    node1 = node1.next;
                }
                
                if(node2 == null) {
                    node2 = pHead1;
                } else {
                    node2 = node2.next;
                }
            }
            
            return node1;
        }
    

    T37. 数字在排序数组中出现的次数

    题目描述

    统计一个数字在排序数组中出现的次数。

    解题思路

    顺序查找。

        public int GetNumberOfK(int[] array , int k) {
            int sum = 0;
            
            for(int val: array) {
                if(val == k) {
                    sum ++;
                }
            }
            
            return sum;
        }
    

    二分查找,找到第一次和最后一次k出现的位置,即可计算次数。

        public int GetNumberOfK(int[] array , int k) {
            int first = getFirstK(array, k);
            int last = getLastK(array, k);
            
            if(first == -1) return 0;
            if(last == -1) return 0;
            
            return last - first + 1;
        }
        
        private int getFirstK(int[] array , int k) {
            int low = 0, high = array.length - 1;
            
            while (low <= high) {
                int mid = (high + low) / 2;
                if(array[mid] >= k) {
                    high = mid - 1;
                } else {
                    low = mid + 1;
                }
            }
            
            if(low > array.length - 1 || array[low] != k) 
                return -1;
            
            return low;
        }
        
        private int getLastK(int[] array , int k) {
            int low = 0, high = array.length - 1;
            
            while (low <= high) {
                int mid = (high + low) / 2;
                if(array[mid] > k) {
                    high = mid - 1;
                } else {
                    low = mid + 1;
                }
            }
            
            if(high < 0 || array[high] != k) 
                return -1;
            
            return high;
        }
    

    项目地址https://github.com/JohnnyJYWu/offer-Java

    上一篇算法 | 一周刷完《剑指Offer》 Day2:第17~26题
    下一篇算法 | 一周刷完《剑指Offer》 Day4:第38~49题

    希望这篇文章对你有帮助~

    相关文章

      网友评论

          本文标题:算法 | 一周刷完《剑指Offer》 Day3:第27~37题

          本文链接:https://www.haomeiwen.com/subject/zpnyhqtx.html