下面的例题来自剑指offer第5章的内容。很多公司的面试官都把代码的时间效率当做一个考查重点。面试官除了考查应聘者的编程能力之外,还关注应聘者有没有不断优化效率、追求完美的态度和能力。
1.数组中出现次数超过一半的数字
题目描述
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
思路:
如果采用暴力方,对array[i]都查看是否该值超过一半,时间复杂度为n的平方,太高。如果先排序再查看时间复杂度为nlogn。也可以直接利用额外的空间一次遍历进行次数统计,找出超过一半的数。
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
int len=array.length;
if(len==0)
return 0;
int key=array[0];
int count=1;
for(int i=1;i<len;i++){
if(array[i]==key)
count++;
else{
if(count>0)
count--;
else{
key=array[i];
count=1;
}
}
}
if(count>0 && checkMoreHalf(array,key))
return key;
return 0;
}
public boolean checkMoreHalf(int[] array,int key){
int count=0;
for(int i=0;i<array.length;i++){
if(array[i]==key)
count++;
}
if(count>array.length/2)
return true;
return false;
}
}
由于该值超过数组的一半那么排序过后该值一定处于中间位置,也就是中位数。可以利用快速排序找partition的方法找到中位数(和找第k大的数类似)。
public int MoreThanHalfNum_Solution(int [] array) {
int len=array.length;
if(len==0)
return 0;
int p=partition(array, 0, len-1);
while(p!=len/2){
if(p>len/2)
p=partition(array,0,p-1);
else
p=partition(array,p+1,len-1);
}
if(checkMoreHalf(array,array[p]))
return array[p];
return 0;
}
public int partition(int[] array,int i,int j){
int x=array[i];
while(i<j){
while(i<j && array[j]>=x)
j--;
if(i<j){
array[i]=array[j];
i++;
}
while(i<j && array[i]<=x)
i++;
if(i<j){
array[j]=array[i];
j--;
}
}
array[i]=x;
return i;
}
public boolean checkMoreHalf(int[] array,int key){
int count=0;
for(int i=0;i<array.length;i++){
if(array[i]==key)
count++;
}
if(count>array.length/2)
return true;
return false;
}
2.最小的K个数
题目描述
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
思路:
和上题一样采用快速排序中partition的思想,时间复杂度是n
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> result=new ArrayList<Integer>();
int len=input.length;
if(len==0 || len<k || k==0)
return result;
int p=partition(input,0,len-1);
while(p!=k-1){
if(p>k)
p=partition(input,0,p-1);
else
p=partition(input,p+1,len-1);
}
for(int i=0;i<=p;i++){
result.add(input[i]);
}
return result;
}
public int partition(int[] array,int i,int j){
int x=array[i];
while(i<j){
while(i<j && array[j]>=x)
j--;
if(i<j){
array[i]=array[j];
i++;
}
while(i<j && array[i]<=x)
i++;
if(i<j){
array[j]=array[i];
j--;
}
}
array[i]=x;
return i;
}
}
这个题类似于Top k问题。所以可以利用容器来保存当前top k,然后一次遍历后面的数,对容器里的top k进行变换,使得容错中的数始终是top k的数。容器可以使用堆、平衡二叉树、红黑数等。时间复杂度是nlogn。
3.连续子数组的最大和
题目描述
HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。你会不会被他忽悠住?
思路:
可以实现动态规划,用个表记录前面位置子数组的最大和,然后利用该表一步步扩大。
也可以采用贪心,取当前和最大的,如下:
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int len=array.length;
if(len==0)
return 0;
int max=array[0];
int sum=array[0];
for(int i=1;i<len;i++){
sum=Math.max(sum+array[i], array[i]);
max=Math.max(max, sum);
}
return max;
}
}
4.整数中1出现的次数(从1到n整数中1出现的次数)
题目描述
求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数。
思路:
如果对1到n每个数都计算其1出现的个数,时间复杂度太高了。这道题需要利用数字特点和规律,计算每一位上1出现的次数:
例如百位上1出现次数,数值n在百位上的值是curNum则:
if(curNum==0)
1出现的次数等于比百位更高位数*100。例如n=1023,高位数就是1,百位上出现1的次数是1*100;
if(curNum==1)
1出现的次数等于比百位更高位数*100,再加上低位上的数,再加1。例如n=1123,高位数就是1,低位数是23,百位上出现1的次数是1*100+23+1;
if(curNum>1)
1出现的次数等于比百位更(高位数+1)*100,例如n=1223,高位数就是1,次数百位上出现1的次数是(1+1)*100;
public class Solution {
public int NumberOf1Between1AndN_Solution(int n) {
int count=0;
int factor=1;
int curNum;
int highNum;
int lowNum;
while(n/factor!=0){
curNum=(n/factor)%10;
lowNum=n%factor;
highNum=n/(factor*10);
if(curNum==0)
count+=highNum*factor;
if(curNum==1)
count+=highNum*factor+lowNum+1;
if(curNum>1)
count+=(highNum+1)*factor;
factor*=10;
}
return count;
}
}
5.把数组排成最小的数
题目描述
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
思路:
可以利用全排列,然后比较所以排序的值找出最小的数,此时时间复杂度是n!。可以利用排序直接排出一个最小的数。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
public class Solution {
public String PrintMinNumber(int [] numbers) {
int len=numbers.length;
if(len==0)
return "";
String[] str=new String[len];
for(int i=0;i<len;i++){
str[i]=numbers[i]+"";
}
Arrays.sort(str, new Comparator<String>(){
@Override
public int compare(String o1, String o2) {
// TODO Auto-generated method stub
String s1=o1+o2;
String s2=o2+o1;
return s1.compareTo(s2);
}
});
String result="";
for(int i=0;i<len;i++){
result+=str[i];
}
return result;
}
}
6.丑数
题目描述
把只包含因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
思路:
如果从1开始每个数都判断是不是丑数,时间复杂度太高。根据丑数的定义,丑数应该是例一个丑数乘以2、3、5的结果。因此我们可以建一个数组,里面的数字是排好序的丑数,每一个丑数都是前面丑数乘以2、3、5得到的。
public class Solution {
public int GetUglyNumber_Solution(int index) {
if(index==0)
return 0;
int[] record=new int[index];
record[0]=1;
int i2=0,i3=0,i5=0;
int i=1;
while(i<index){
int tmp=Math.min(record[i2]*2, record[i3]*3);
record[i]=Math.min(record[i5]*5, tmp);
while(record[i2]*2<=record[i])
i2++;
while(record[i3]*3<=record[i])
i3++;
while(record[i5]*5<=record[i])
i5++;
i++;
}
return record[index-1];
}
}
7、第一个只出现一次的字符
题目描述
在一个字符串(1<=字符串长度<=10000,全部由大写字母组成)中找到第一个只出现一次的字符,并返回它的位置
思路:
这题没想到什么特别的方法,只能利用HashMap进行统计字符出现的次数,找出第一个次数为1的字符。
import java.util.Map;
import java.util.HashMap;
public class Solution {
public int FirstNotRepeatingChar(String str) {
Map<Character,Integer> map=new HashMap<>();
int len=str.length();
for(int i=0;i<len;i++){
char c=str.charAt(i);
int count=1;
if(map.containsKey(c))
count+=map.get(c);
map.put(c, count);
}
for(int i=0;i<len;i++){
if(map.get(str.charAt(i))==1)
return i;
}
return -1;
}
}
8.数组中的逆序对
题目描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
输入描述:
题目保证输入的数组中没有的相同的数字
数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5
输入例子:
1,2,3,4,5,6,7,0
输出例子:
7
思路:
如果采用暴力法,对每个array[i]都统计一遍逆序对,时间复杂度是n的平方。我们可以利用归并排序的思想(对一个有序的的数组排序排序时间复杂度更低),所以这里对利用有序的数组算逆序对更快。
public class Solution {
public int InversePairs(int [] array) {
int from=0;
int to=array.length-1;
return countPairs(array, from,to)%1000000007;
}
public int countPairs(int[] array,int from,int to){
int count=0;
if(from<to){
int mid=(from+to)/2;
count=countPairs(array,from,mid);
count+=countPairs(array,mid+1,to);
int[] newarray=new int[to-from+1];
int i=from;
int j=mid+1;
int m=0;
while(i<=mid && j<=to ){
if(array[i]>array[j]){
count+=mid-i+1;
if(count>1000000007)//数值过大求余
{
count%=1000000007;
}
newarray[m++]=array[j++];
}
else{
newarray[m++]=array[i++];
}
}
while(i<=mid){
newarray[m++]=array[i++];
}
while(j<=to){
newarray[m++]=array[j++];
}
m=0;
for(i=from;i<=to;i++){
array[i]=newarray[m++];
}
}
return count;
}
}
9、两个链表的第一个公共结点
题目描述
输入两个链表,找出它们的第一个公共结点。
思路:
如果两个链表出现一个公共节点,那么之后的节点都相同。这道题可以先计算两个链表的长度,计算出长度的差n,然后从头遍历两个链表,让较长的链表先走n步。那么两个链表将会在第一个公共节点相遇。
也可以利用空间换时间,利用一个set统计一个链表中出现的节点,然后遍历另一个链表,找出第一个重复的节点。
import java.util.HashSet;
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
HashSet<ListNode> set=new HashSet<>();
ListNode pNode=pHead1;
while(pNode!=null){
set.add(pNode);
pNode=pNode.next;
}
pNode=pHead2;
while(pNode!=null){
if(set.contains(pNode))
return pNode;
pNode=pNode.next;
}
return null;
}
}