剑指offer5-优化时间和空间效率

题目-数组中出现次数超过一半的数字

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

思路

1.数组中有一个数字出现的次数超过数组长度的一半==它出现的次数比其他所有数字出现次数的和还要多;

因此我们考虑一种思路,定义一个count。如果数字相同,则count++;如果数字不同,则count--;

这样的话,最后存储的序列元素m,就是这个序列中最多的元素。

2.上面这种思路其实就是Boyer-Moore Majority Vote Algorithm(摩尔投票算法),一种在线性时间O(n)和空间复杂度的情况下,在一个元素序列中查找包含最多的元素。属于流算法(streaming algorithm)

即从头到尾遍历数组,遇到两个不一样的数就把两个数同时去掉,去掉的数可能都不是m,也可能一个是m。但是因为m出现的次数大于总长度的一半,所以删完了最后剩下的数是m。

本题是摩尔投票算法最简单的形式。

3.本题需要注意的问题是:该序列中不一定存在长度大于数组一半的数,也就是之前存储的序列中最多元素m,长度不一定符合要求。由于之前已经找出了这个数字,那么就再次验证,判断该数字的出现次数是否>数组一半的长度。

解法

public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        int m = array[0];
        int count=0;//初始化状态下计数器为0
//算法依次扫描序列中的元素
for(int i=0;i<array.length;i++){
       //处理元素x时,如果计数器为0,将x赋值给m。
       if(count==0){ m=array[i]; count++; } else if(array[i]==m){ count++; } else count--; } //如果不存在长度大于数组一半的数,则输出0 //第二次遍历 count = 0; for(int v:array){ if(v == m) count++; } if(count<=array.length/2){ return 0; } return m; } }

 

题目-最小的K个数

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

思路

1.本题最简单的思路就是把输入的n个整数排序,排序后位于数列最前面的k个数就是所求。此时时间复杂度是O(nlogn)

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

当且仅当允许修改数组元素时才可以使用。复杂度是O(N)+O(1)

3.快排的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

4.如果不允许修改数组元素&处理海量数据的场景。那么采用维护大小为K的最小堆来实现。复杂度:O(NlogK)+O(K)

具体:在添加一个元素之后,如果大顶堆的大小大于K,那么需要将大顶堆的堆顶元素去除。

解法

import java.util.ArrayList;
 
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> ret = new ArrayList<>();
        if(k > input.length || k <= 0)
            return ret;
        findKthSmallest(input, k-1);
     //findKthSmallest会改变数组,使得前k个数都是最小的k个数
for(int i=0; i<k ;i++) ret.add(input[i]); return ret; } public void findKthSmallest(int[] input, int k){ int low=0, high=input.length-1; while(low < high){ //找寻基准数据的正确索引 int j=partition(input, low, high); if(j==k) break; //进行迭代对index之前和之后的数组进行相同的操作使整个数组有序 if(j>k) high=j-1; else low=j+1; } } //分区操作,结束该分区后,基准数就放在数列的中间位置 private int partition(int[] input, int low, int high){ //基准数据 int p = input[low]; int i=low, j=high+1; while(true){ //从后往前找,找到比p小的第一个数 while(i!=high && input[++i]<p); //从前往后找,找到比p大的第一个数 while(j!=low && input[--j]>p); //直到i>=j,第一回结束。此时p之前的数都小于p,p之后的数都大于p if(i >= j) break; swap(input, i, j); } swap(input, low, j); return j; } private void swap(int[] input, int i, int j){ int t = input[i]; input[i] = input[j]; input[j] = t; } }

 

题目-连续子数组的最大和

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

思路 

1.分析数组的规律。当从头到尾累加数组的数字,初始化和为0。如对于数组{6,-3,-2,7,-15,1,2,2},首先加上第一个数字6,此时和为6;再加上-3,-2,7和为8;再加上-15,此时和为-7,小于0,如果用小于0的数字去加后面的数,还不如抛弃前面这段,直接从1开始。再设置一个greatestSum用来记录当前最大子数组的和。

2.32位的int,正数的范围是(0,0x7FFFFFFF),负数(0x80000000,0xFFFFFFFF)。所以设置int最小值为0x80000000

解法

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        boolean invalidInput = false;
        if((array.length<=0) || (array==null) ){
            invalidInput = true;
            return 0;
        }
        int curSum=0;
        //32位的int,正数的范围是(0,0x7FFFFFFF),负数(0x80000000,0xFFFFFFFF)
        int greatestSum=0x80000000;
        for(int i=0; i<array.length; i++){
            if(curSum<=0){
                curSum=array[i];
            }
            else
                curSum=curSum+array[i];
            if(curSum>greatestSum)
                greatestSum=curSum;
        }
        return greatestSum;
    }
}

 

题目-整数中1出现的次数(从1到n整数中1出现的次数)  

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

思路

1.最直观的方式是:累加,通过每次对10求余判断,整数的个位数字是否为1.如果这个数字大于10,那么再次除以10.

这种方式,如果输入n,n为O(logn)位,需要判断每一位是不是1,那么时间复杂度是O(n*logn)

解法

public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        if(n<0)
            return 0;
        int count=0;
        for(int i=1;i<=n;i++){
            count+=numberOf1(i);
        }
        return count;
    }
     
    private static int numberOf1(int n){
        int count=0;
        while(n>0){
            if(n%10 == 1){
                ++count;
            }
            n=n/10;
        }
        return count;
    }
}

 

题目-把数组排成最小的数 

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

思路

1.这个题目标就是找到一个排序规则,数组根据这个规则排序之后能排成一个最小的数字。

要确定排序规则,就是比较两个数字中,哪个应该排在前面。即哪个排在前面形成的数字会更小。

2.解决大数问题首要的方式就是:数字转化成字符串,nums[i]=numbers[i]+"";比较的时候按照字符串大小的比较规则就可以

3.数组的排序函数

解法

import java.util.Comparator;
import java.util.Arrays;
 
public class Solution {
    public String PrintMinNumber(int [] numbers) {
        int len=numbers.length;
        if(numbers == null || len == 0)
            return "";
        //数字转成字符串
        String[] nums = new String[len];
        for(int i=0; i<len; i++){
            nums[i]=numbers[i]+"";
        }
        //进行排序,来实现得到更小的数字
        Arrays.sort(nums, new Comparator<String>(){
            public int compare(String str1, String str2){
                String c1 = str1+str2;
                String c2 = str2+str1;
                return c1.compareTo(c2);
            }
        });
        String ret="";
        for(int i=0;i<len;i++){
            ret=ret+nums[i];
        }
        return ret;
    }
}

  

题目-丑数 

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

思路

1.因子的概念:一个数 m 是另一个数 n 的因子,是指 n 能被 m 整除,也就是n%m==0。

2.最直观的方式就是:逐个判断每个整数是不是丑数。但是这种方式的时间效率很低

3.那么2中提到的方式,存在的问题是不管一个数是否为丑数,都要计算。那就为了优化这种方式,创建数组仅保存已经找到的丑数。以空间换时间。

由于丑数的定义是:另一个丑数乘以2、3或5的结果。

那么我们可以创建一个数组,用来保存数组里已有丑数,再将数组里的数乘以2、3或5。但是需要做的是把找到的丑数进行排序。

如果当前数组中最大的丑数是M,下一个要生成的丑数一定是数组中某一个丑数乘以2、3或5的结果。所以首先需要考虑的是把已有的每个丑数乘以2,我们只获取第一个大于M的结果。

但其实并不是需要把每个丑数都分别乘以2、3或5。由于现有数组是有序存放的,那么对乘以2而言,肯定存在一个丑数T2,排在它之前的每个丑数乘以2的结果 都会 小于 已有最大的丑数。而排在它之后的每个丑数乘以2的结果 都会 太大。那么我们只需要记下这个丑数的位置,每次生成新的丑数时,就去更新T2。对乘以3和5而言,也存在同样的T3和T5.

4.可以看成3个数组的合并,A:{1*2,2*2,3*2,4*2,5*2,6*2,8*2,10*2......};B:{1*3,2*3,3*3,4*3,5*3,6*3,8*3,10*3......};C:{1*5,2*5,3*5,4*5,5*5,6*5,8*5,10*5......}

如果下一个数,是某个数组中的,那么就把对应数组的指针往后移一位,

 

需要注意的是:如果这个数在多个数组中出现,那么这多个数组中每个数组的指针都需要往后移一位

可以用动态规划来理解这个问题,dp[i]表示第i个丑数,前面的每个状态都可以*2/*3/*5来形成一个新的状态,每次都选择最小的那个数来作为新的状态,也就是下一个丑数。

解法

public class Solution {
    public int GetUglyNumber_Solution(int index) {
        if(index <= 0)
            return 0;
        int[] uglyArray = new int[index];
        uglyArray[0]=1;
     //有资格同i相乘的最小丑数的位置
int multiply2 = 0; int multiply3 = 0; int multiply5 = 0; //按照大小生成丑数,这里的所有数都是丑数 for(int i=1;i<index;i++){ uglyArray[i] = min(uglyArray[multiply2]*2, uglyArray[multiply3]*3, uglyArray[multiply5]*5);
       //如果一个丑数 dp[pi] 通过乘以 i 可以得到下一个丑数,那么这个丑数 dp[pi] 就永远失去了同 i 相乘的资格(没有必要再乘了),我们把 pi++ 让 dp[pi] 指向下一个丑数即可。
if(uglyArray[multiply2]*2 == uglyArray[i]) multiply2++; if(uglyArray[multiply3]*3 == uglyArray[i]) multiply3++; if(uglyArray[multiply5]*5 == uglyArray[i]) multiply5++; } return uglyArray[index-1]; } public int min(int num1, int num2, int num3){ int min = (num1<num2)?num1:num2; return min<num3?min:num3; } }

 

题目-第一个只出现一次的字符

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

如输入“abaccdeff”,输出“b”

思路

1.最直观的方式就是使用HashMap对每个字符在字符串中出现的次数进行统计,但是要统计的字符范围有限,因此可以使用整型数组代替HashMap。

需要从头到尾扫描字符串两次,第一次扫描字符串时,每扫描到一个字符就在数组的对应项中把次数加1;

第二次扫描,每扫描到一个字符就能得到该字符的出现次数。那么第一个只出现一次的字符就是符合要求的输出。

2.优化的方式是:考虑到只需要找到只出现一次的字符,那么需要统计的次数信息只有0,1或更大,使用两个bit位就可以存储信息。

BitSet bs2 = new BitSet(256);

使用bs2.get(char )/bs2.set(char )来进行设置和提取操作

解法

public class Solution {
    public int FirstNotRepeatingChar(String str) {
        if(str == null){
            return -1; 
        }
        int[] cnts = new int[256];
        for(int i=0;i<str.length();i++){
            cnts[str.charAt(i)]++;
        }
        for(int i=0;i<str.length();i++){
            if(cnts[str.charAt(i)]==1)
                return i;
        }
        return -1;
    }
}
class Solution {
    public char firstUniqChar(String s) {
        char ret=' ';
        Map<Character,Integer> map=new HashMap<Character,Integer>();
        for(int i=0;i<s.length();i++){
            //获取当前元素
            char temp=s.charAt(i);
            if(!map.containsKey(temp)){
                map.put(temp,1);
            }
            else{
                map.put(temp,2);
            }
        }
        for(int i=0;i<s.length();i++){
            if(map.get(s.charAt(i))==1){
                return s.charAt(i);
            }
        }
        return ret;
    }
}

 hashmap的使用方法:

建立hashmap:HashMap<Character,Integer> map=new HashMap<>();

增:map.put('c',4);

删:map.remove('c');

改:map.put('c',6);

查:map.containsKey('d');  & map.containsValue(4);

根据key找value:map.get(key);

遍历:1.使用keySet(),当只需要map中的键/值:Set<Character> keys=map.keySet();//返回key的集合  for(char key:keys){int value=map.get(key);//返回key对应的value值}

for(Integer value:map.values()){ int x=value;//得到所有的value值}

2.使用entryKey():Set<Entry<Character,Integer>> entrySet=map.entrySet();//返回所有键值对的集合   

for(Entry<Character,Integer> entry:entrySet){char key=entry.getKey(); int value=entry.getValue;}

 

题目-数组中的逆序对

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

思路

举例统计数组{7,5,6,4}中逆序对的过程:逆序对为(7,5),(7,6),(7,4),(5,4),(6,4)。

合并两个子数组并统计逆序对的过程:

那么整体思路就是:

1.先把数组分割成两个子数组,统计出子数组内部的逆序对的数目,再统计出两个相邻子数组之间的逆序对的数目。

2.在统计逆序对的过程中,还需要对数组进行排序。(因为如果是排序后,那么如果左数组的第一个数大于右数组的第一个数,那么左数组中每个数与右数组的每个数肯定都能构成逆序对)

3.那么这个过程其实就是归并排序。

解法

public class Solution {
    private long reversePair = 0;
    public int InversePairs(int [] array) {
        int len = array.length;
        //数组为null/长度为0
        if(array == null || len == 0){
            return 0;
        }
        int[] p = new int[array.length];
        //排序
        mergeSort(array, 0, len-1, p);
        return (int)(reversePair%1000000007);
    }
    public void mergeSort(int [] array, int first, int last, int temp[]){
        if(first<last){
            int mid = (first+last)/2;
            //左边有序
            mergeSort(array, first, mid, temp);
            //右边有序
            mergeSort(array, mid+1, last, temp);
            //将左右两个有序数列合并    
            mergeArray(array, first, mid, last, temp);
        }
    }
    //将两个有序数列array[first,...,mid]和array[mid,...,last]合并
    public void mergeArray(int[] array, int first, int mid, int last, int[] temp){
        int first1 = first, end1 = mid;
        int first2 = mid+1, end2 = last;
        int p = 0;
        while(first1 <= end1 && first2 <= end2){
            //左数组大于右数组
            if(array[first1]>array[first2]){
                temp[p++] = array[first2++];
                //由于当前数组都是有序数组,如果当前左数组元素大于右数组元素array[first2]
                //那么就会有当前左数组后面的元素,都大于array[first2]
                reversePair+=mid-first1+1;
            }
            //如果左数组的数字 小于或等于 右数组的数字,不构成逆序对
            //每一次比较时,都把较大的数字从后往前复制到一个辅助数组中,确保辅助数组中的数组增序
            //把较大的数字复制到数组之后,把对应的指针向前移动一位,进行下一轮的比较
            else{
                temp[p++]=array[first1++];
            }
        }
        //两个数组相互比较大小,其中一个数组比较完了。还剩左边数组还有一些数字,那么直接添加到temp后面
        while(first1 <= end1){
            temp[p++] = array[first1++];
        }
        while(first2 <= end2){
            temp[p++] = array[first2++];
        }
        //此时的array是合并之后,有序的
        for(int i=0; i<p; i++){
            array[first+i]=temp[i];
        }
    }
}

  

题目-两个链表的第一个公共结点

输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的) 

思路

 

由链表结点的定义,看出两个链表是单向链表,如果单向链表有公共的结点,那么这两个链表从某一结点开始,它们的next都指向同一个结点。并且单向链表的每个结点都只有一个next,那么从第一个公共结点开始,它们的next都是指向同一结点,因此之后的所有结点都是重合的,不可能再出现分叉。

由于两个链表的长度可能不一致,因此为了让两个链表的指针同时到达交点,需要让长链表先走一段距离,再同时在两个链表上遍历。

如果访问链表A的指针访问到链表尾部,那么从链表B的头部开始重新访问链表B。//以此实现让长链表先走一个结点。

 

由于设第一个链表没交集的一段=a,第二个链表没交集的一段=b,交集一段=c。

那么a+c+b=b+c+a,因此两个链表会在交集处相遇。

解法

/*
public class ListNode {
    int val;
    ListNode next = null;
 
    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        ListNode l1 = pHead1, l2 = pHead2;
        //使得A和B两个链表的指针能同时访问到交点。即长的先走一段距离,当长度相等时一起向前走
        while(l1 != l2){
            //如果访问链表A的指针访问到链表尾部,那么从链表B的头部开始重新访问链表B
            l1 = (l1==null)?pHead2:l1.next;
            //如果访问链表B的指针访问到链表尾部,那么从链表A的头部开始重新访问链表A
            l2 = (l2==null)?pHead1:l2.next;
        }
        return l1;
    }
}

 

posted @ 2020-03-02 20:31  闲不住的小李  阅读(238)  评论(0编辑  收藏  举报