【剑指Offer】优化时间和空间效率(上)

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

题目描述

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

分析

由于一个数字的出现次数超过了总长度的一半,所以最直观的想法就是先对数组进行排序,假设数组的长度为 n n n,则下标为 n / 2 n/2 n/2的元素一定是所求数字。
但是排序的时间复杂度为O( n n nlog n n n),所以需要更快的解法。
根据数组的特点,我们可以知道,所求数字出现的次数比其他所有数字出现的次数都多,所以可以使用两个变量来辅助求解:一个变量保存数字,一个变量保存次数
依次遍历,如果读取的新数字和保存的数字相同,则次数+1;否则,次数-1。若次数为0,则将遍历的下一个数字保存下来,并把次数保存为1。
由于目标数字出现的次数超过了一半,所以最后保存的数字一定就是目标数字。
该解法时间复杂度为O(n)

具体如下:
在这里插入图片描述
更直观的理解,假设有10个数字{ 1, 2, 1, 3, 1, 4, 1, 5, 1, 1},其中有6个数字为1,其他4个数组分别为2,3,4,5。假设每个不为1的数字都分别和一个1抵消,则最后剩下两个1,相当于上例中最后保存下的数字=1,次数=2。

代码

import java.util.Arrays;

public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        int num = 0;
        int cnt = 0;
        for(int x: array){
        for(int x: array){
            if(cnt>0 && x != num)
                cnt--;
            else if(cnt++ == 0)
                num = x;
        }
        // 输入的数组不符合条件的情况分析
        // case1: 最终cnt为0,说明没有一个数次出现的次数超过长度的一半。
        // 例:1、2、1、4、3、2
        if(cnt == 0)
            return 0;
        // case2: 最终的cnt=1,可能的情况有两种:
        // 第一:该数字就是目标数字,total*2>array.length
        // 第二:array.length是奇数,前面偶数个数字相互抵消使得cnt=0,
        // 但最后一个数字并不是目标数字.
        // 例:2、1、1、5、3、2、6,此时num=6,不正确。
        if(cnt == 1){
            int total = 0;
            for(int x : array)
                if(x == num)
                    total++;
            if(total * 2 <= array.length)
                return 0;
        }
        return num;
    }
}

最小的K个数

题目描述

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

分析

利用Partition函数可以实现。
基本思路就是经过对第K个数字 K t h K^{th} Kth排序之后, K t h K^{th} Kth左边的数字都比它小,右边的数字都比它大。注意!整个数组并不一定是正确排序的。
Partition函数是快速排序的核心,时间复杂度为O(log n n n)。

代码

import java.util.Arrays;
import java.util.ArrayList;

public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> res = new ArrayList<Integer>();
        if(k > input.length || k == 0 || input == null)
            return res;
        int start = 0;
        int end = input.length-1;
        int idx = Partition(input, start, end);
        while(idx != k-1){
            if(idx < k-1)
                start = idx + 1;
            else
                end = idx - 1;
            idx = Partition(input, start, end);
        }
        
        for(int i=0; i<k; i++)
            res.add(input[i]);
        return res;
    }
    
    public int Partition(int[] input, int start, int end){
        int key = input[start];
        while(start < end){
            while(start<end && input[end]>=key)
                end--;
            swap(input, start, end);
            while(start<end && input[start]<=key)
                start++;
            swap(input, start, end);
        }
        return start;
    }

    public void swap(int[] input, int idx1, int idx2){
        int temp = input[idx1];
        input[idx1] = input[idx2];
        input[idx2] = temp;
    }
}

连续子数组的最大和

题目描述

输入一个整型数组,数组里有正数也有负数。数组中一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O( n n n)。
例如{1, -2, 3, 10, -4, 7, 2, -5}的最大和是由子数组{3, 10, -4, 7, 2}的各元素相加得到的。

分析

从前往后遍历,使用两个辅助变量:当前的子数组和、最大子数组和。具体过程如下:
在这里插入图片描述
由于这个整数数组有正有负,所以最大的子数组和肯定是大于0的。
当子数组和小于0的时候,例如第2步,则前面的元素之和为-1<0,对求最大子数组和没有积极作用,则更新子数组和为0。
最大子数组和的最终值就是所求解。

参考:浙江大学-数据结构(陈越、何钦):https://www.bilibili.com/video/av43521866/?p=8

代码

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        int res = 0;
        if(array.length == 0)
            return 0;
        else 
            res = array[0];
        
        int sum = 0;
        for(int i=0; i<array.length; i++){
            sum += array[i];
            if(sum > res) 
                res = sum;
            if(sum < 0) 
                sum = 0;
        }
        return res;
    }
}

从1到n整数中1出现的次数

题目描述

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

分析

可以通过分析数字的规律来解这道题。
例如21304这个数字,
万位上的1的数目是10000-19999,一共是 1 0 5 10^5 105个;
千位上出现1的数字为:21000-21304、11000-11999,一共是( 1 0 4 10^4 104+305)个;
百位上出现1的数字为:21100-21199、20100-20199、……、1100-1199、100-199,一共是 22 × 1 0 3 22\times10^3 22×103个;
十位上出现1的数字为:21210-21219、21110-21119、……、110-119、10-19,一共是213 × 1 0 2 \times10^2 ×102个;
个位上出现1的数字为:1、11、21、……、21291、21301,共 2131 × 10 2131\times10 2131×10个。

规律总结:
假设一个数字N = XaY
在这里插入图片描述
假设a所在的数量级是 1 0 i 10^i 10i(从低往高, i = 0 , 1 , 2 , . . . i=0,1,2,... i=0,1,2,...),即N = X × 1 0 ( i + 1 ) \times10^{(i+1)} ×10(i+1) + a × 1 0 i \times10^{i} ×10i + Y

  • a = 0时:则第 i i i位出现1的数字共有: X × 1 0 i \rm{X}\times10^i X×10i个;
  • a = 1时:则第 i i i位出现1的数字共有: X × 1 0 i + Y + 1 \rm{X}\times10^i+Y+1 X×10i+Y+1个;
  • 2 ≤ \le a ≤ \le 9时:则第 i i i位出现1的数字共有: ( X + 1 ) × 1 0 i \rm{(X+1)}\times10^i (X+1)×10i个;

代码

public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        if(n == 0) 
            return 0;
        
        int cnt = 0;
        int i = 1;
        while(n/i != 0){
            int y = n % i; //低位
            int a = (n/i) % 10; //当前位
            int x = n / (i*10); //高位 
            if(a==0)
                cnt += x*i;
            else if(a==1)
                cnt += x*i + y + 1;
            else
                cnt += (x+1)*i;
            i *= 10;
        }
        return cnt;
    }
}

把数组排成最小的数

题目描述

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

分析

最直观的方法就是将所有可能的情况列出来,然后进行比较,但是要考虑到两个问题:

  1. 组成的数字太大,超过了int或者long表示的范围;
  2. n个数字有n!种排列的可能,效率低下。

可以直接考虑字符串大小比较的情况。
字符串a和字符串b有两种排列组合的可能,如果

  • ab < ba,说明a<b;
  • ab > ba,说明a>b;
  • ab = ba,说明a=b。
    则按照这样的规律,对字符串数组进行“从小到大”的排列。如图中的例子(冒泡):
    在这里插入图片描述
    排列之后,从前往后将字符串连接起来,形成一个新的数字字符串即为所求。

代码

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class Solution {
    public String PrintMinNumber(int [] numbers) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        String res = "";
        for(int i : numbers)
            list.add(i);
        // 排序
        Collections.sort(list, new Comparator<Integer>(){
            public int compare(Integer x, Integer y){
                String str1 = x + "" + y;
                String str2 = y + "" + x;
                return str1.compareTo(str2);
            }
        });
        
        for(int i:list)
            res += i;
        
        return res;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值