时间效率
数组中出现次数超过一半的数字
题目描述
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为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。
分析
最直观的方法就是将所有可能的情况列出来,然后进行比较,但是要考虑到两个问题:
- 组成的数字太大,超过了int或者long表示的范围;
- 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;
}
}