文章目录
构建乘积数组
题目描述:
给定一个数组A[0,1,…,n-1],请构建一个数组B[0,1,…,n-1],其中B中的元素B[i]=A[0]A[1]…*A[i-1]A[i+1]…*A[n-1]。不能使用除法。(注意:规定B[0] = A[1] * A[2] * … * A[n-1],B[n-1] = A[0] * A[1] * … * A[n-2];)
题目分析:
观察发现,B[i]的结果为A[0:length-1]中除去A[i]元素的乘积。
public class Solution {
public int[] multiply(int[] A) {
int[] B=new int[A.length];
for(int i=0;i<B.length;i++){
B[i]=1;
}
for(int i=0;i<B.length;i++){
for(int j=0;j<A.length;j++){
if(i==j){
continue;
}
B[i]*=A[j];
}
}
return B;
}
}
数组中重复的数字
题目描述:
长度为n的数组中元素大小均为0~n-1,判断该元素是否存在重复的元素,并且输出这个元素到一个数组duplication[ 0 ]
解题思路:
设置一个count[]数组专门用于记录每一个元素的数量,然后遍历这个count数组,如果找到count [ number[ i ]]>=2,则将该元素number[i]赋值给duplication[0],这其实就是java中使用数组来模拟输出元素。
public class Solution {
// Parameters:
// numbers: 输入数组
// length: 输入数组的长度
// duplication: 输出数组,用于记录找到的任意一个重复的数字
// Return value: 找了则为True,否则为False
public boolean duplicate(int numbers[],int length,int [] duplication) {
if(length==0){
return false;
}
int[] count=new int[length];
for(int i=0;i<length;i++){
count[numbers[i]]++;
}
for(int i=0;i<count.length;i++){
if(count[numbers[i]]>=2 ){
duplication[0]=numbers[i];
return true;
}
}
return false;
}
}
和为sum的两个数字
题意描述:
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
解题思路:
由于数组已经是有序的,设置left指针指向数组头,right指针指向数组尾,两者相向夹逼,在夹逼的过程中,会出现以下三种情况。
(1)arr[left ]+arr[right ]==sum,表明找打,且两数相差越大,乘积越小,所以只要发现,则这个值必然符合乘积最小的要求。
(2)arr[left ]+arr[right ]>sum,此时需要增到其中一个加数,只能是left++(因为数组递增);
(3) arr[left ]+arr[right ]< sum,需要减少一个加数,只能是right–;
//按照上面的分析,这里的代码可以在找到符合要求的left和right直接添加进返回集合中,但是,这里给出一个判断的步骤。仅供参考。
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
ArrayList<Integer> res=new ArrayList<Integer>();
if(array.length<2){
return res;
}
int left=0;
int right=array.length-1;
int s=0;
int minNum1=-1;//记录最小乘积对应的left;
int minNum2=-1;//记录最小乘积对应的right;
int min=Integer.MAX_VALUE;//初始最小值。
while(left<right){
s=array[left]+array[right];
if(s<sum){
left++;
}else if(s>sum){
right--;
}else{
if(array[left]*array[right]<=min){
min=array[left]*array[right];
minNum1=left;
minNum2=right;
}
left++;//继续寻找下一对
right--;
}
}
//只有最小的才进入结果集合。
if(minNum1>=0 && minNum2>=0){
res.add(array[minNum1]);
res.add(array[minNum2]);
}
return res;
}
}
数组中只有唯一一个元素重复
题目描述:
数组大小为n+1,其元素为1~n,其中只有一个元素重复了,其他元素均出现一次,请设计算法找出这个元素。
解题思路:
使用异或操作符来实现。它有三个性质
(1)n^n=0
(2)n^0=n
(3)满足交换律,abc=a(bc)
根据上面三个性质我们可以设计如下算法,假设n=1000,即在{1,2,3,…,n,n,…1000}构成的数组中找到n。
- 设T=123…nn…1000=123…^1000(去除了重复的元素n)
- 设Q=123…n…^1000(只含有一个n)
- 根据运算规则,T^Q=n;
根据上述公式我们可以编写如下代码
class Solution{
public int finDifferent(int[] arr){
int T=0;
int Q=0;
//T=1^2^3^...^n^n^...^1000
for(int i=0;i<arr.length;i++){
T ^= arr[i];
}
//Q=1^2^3^...^n^...^1000
for(int i=1;i<arr.length;i++){
Q^=i;
}
return T^Q;
}
}
数组中只出现一次的两个数字
题目描述:
一个数组中,只有两个数字没有重复,其余的数字均重复一次,请设计算法找出这两个没有重复的数字。
解题思路:
这道题解题的思路和上一题的核心思想一样。我们以number=[1,2,2,3,3,4 ]为例进行说明。
- 设T=122334=14
- 若能number分成两个子数组a=[1, 3,3]和b=[4,2, 2],那么我们可以通过Ta=14(122)=4,Tb=14(4,3,3)=1
- 关键在于如何将number划分为a和b两个数组?
- 由于T=14(两个数不相同,则T必然不为0)转化成二进制后,至少有一个位(N)是1,并且1和4的二进制的第N位必然是不同的(根据运算规则),那么可以根据二进制的N位是否为1来将数组划分为两组。上个案例中划分后的可能结果是:a=[1, 3,3] b=[2, 4,4],最后133=1 244=4,获得最后的结果。
//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
int T=0;
//所有数^的结果
for(int i=0;i<array.length;i++){
T^=array[i];
}
//找到T中的二进制中为1的位
int index=findOneBit(T);
//遍历数组,并分组
num1[0]=0;
num2[0]=0;//0^n=n
for(int i=0;i<array.length;i++){
if(isBitOne(array[i],index)){
//a组,和T进行迭代^运算
num1[0]^=array[i];
}else{
num2[0]^=array[i];
}
}
}
//找到num的二进制的位为1的索引,
//如:00100 返回 2
private int findOneBit(int num){
int index=0;
while(( num & 0x1)==0){
num=num >> 1;
index++;
}
return index;
}
//判断num的第index位是否为1
private boolean isBitOne(int num,int index){
//将num左移index位,然后& 0x1
num=num >> index;
return (num & 0x1)==1?true:false;
}
}
有序数组统计个数
题目描述:
统计有序数组中指定值的个数,要求时间复杂位logn
解题思路:
看到logn,条件反射地想到二分法,只是要做一个小小的改进,在找到k==array[mid ]之后,不能立即返回,应该继续以这个点分别向左和向右继续寻找,当前count+1,无论向左还是向右,只要找到了count++
(1)向左的边界不能< 0,向右继续寻找的边界不能>arr.length-1
public class Solution {
public int GetNumberOfK(int [] array , int k) {
int count=0;
int l=0;
int r=array.length-1;
while(l<=r){
int mid=(l+r)/2;
if(array[mid]>k){
//左半区域
r=mid-1;
}else if(array[mid]<k){
l=mid+1;
}else{
//当前mid符合,count+1;
count++;
//向mid的继续向左边扫描
int tmp=mid-1;
while(true){
if(tmp<0 || k!=array[tmp]){
break;
}
count++;
tmp--;
}
//向右边继续扫描寻找
tmp=mid+1;
while(true){
if(tmp>array.length-1 || k!=array[tmp]){
break;
}
count++;
tmp++;
}
return count;
}
}
return 0;
}
}
使奇数处于偶数的前面
题目描述:
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
解题思路:
参考插入排序的解法
(1)从第2个奇数开始,参考插入排序的方法,将该奇数插入到该奇数之前的第一个偶数位置处。
(2)插入过程,参考插入排序的代码。
public class Solution {
public void reOrderArray(int [] array) {
int len=array.length;
int i=0;
while(i<len){
if(array[i]%2==0){
i++;
}else{
//如果是奇数,array[j]为奇数
if(i==0){
i++;
continue;
}
//从第二个数开始,将当前奇数插入到最前一个偶数位置
//这段代码参考插入排序的代码
int j=i;
int tmp=array[i];
while(j-1>=0 && array[j-1]%2==0){
array[j]=array[j-1];
j--;
}
//此时,j指向最前的一个偶数位置。当前奇数插入到该位置
array[j]=tmp;
//继续遍历
i++;
}
}
}
}
顺时针打印矩阵
题目描述:
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
解题思路:
设置4个边界,up、down、left、right,分别完成up边界向右、right边界向下、down边界向左,left边界向上的遍历,每完成一个边界的遍历,对应的边界去除,同时需要满足left<=right;up<=down;程序以这种逐步缩小边界的方式完成顺时针的打印。
直接欣赏一段代码
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printMatrix(int [][] matrix) {
ArrayList<Integer> res=new ArrayList<Integer>();
//特判,行、列、本身为空,均返回空值。
if(matrix.length==0 || matrix[0].length==0 || matrix==null){
return res;
}
//定义四个边界left、right、up、down
int left=0;
int right=matrix[0].length-1;
int up=0;
int down=matrix.length-1;
while(true){
//最上边一行,向右扫描,col++;
for(int col=left;col<=right;col++){
res.add(matrix[up][col]);
}
//除去最上面一行,up--;
up++;
//不能超出边界
if(up>down){
break;
}
//向下扫描最右边的一列,row++
for(int row=up;row<=down;row++){
res.add(matrix[row][right]);
}
right--;
if(right<left){
break;
}
//向左扫描最后一行
for(int col=right;col>=left;col--){
res.add(matrix[down][col]);
}
down--;
if(down<up){
break;
}
//向上扫描最左边的一列
for(int row=down;row>=up;row--){
res.add(matrix[row][left]);
}
left++;
if(left>right){
break;
}
}
return res;
}
}
数组中出现超过一半的元素
题目描述:
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
解题思路:
候选法则,核心原理:相同元素则+1,不同则-1,如果超过半数,那最后的计数器必然>1,
(1)初始化一个候选人,并给它一张票
(2)统计这个候选人是否重复出现在数组中,每出现一次,count+1;没有出现,表明票给了其他人,则count-1;
(3)当票数再次变为0时,就选举当前元素。
(4)检查最后的候选人出现的次数,如果超过半数,就输出该候选人,否则输出0;
看一段代码什么都明白,所有的文字解释显得苍白无力.
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
//采用候选人法
int con=-1;//候选人
int count=0;//票数
//一张票只能给一个人。
for(int i=0;i<array.length;i++){
if(count==0){//票数为零了,则选举当前元素为候选人,并获得选票
con=array[i];
count++;
}else{
//有后选人获得选票
if(con==array[i]){
count++;
}else{
count--;
}
}
}
//经过一轮头投票之后,能找到候选人
count=0;
//统计该候选人的票数
for(int i=0;i<array.length;i++){
if(con==array[i]){
count++;
}
}
//超过半数输出候选人
return count>array.length/2?con:0;
}
}
top K 问题
题目描述:
从数组中找出最小的k个数
解题分析:
这是典型的top K问题,使用优先队列来模拟大顶堆,大顶堆有如下特点:每次出队的元素都是最大的元素,利用这个特性,我们可以设置一个容量为K的大顶堆,按照数组的顺序入队k个元素,然后后面的元素依次与大顶堆中的元素比较,把小的元素替大顶堆中最大的元素,这样操作下来,就能保证大顶堆中的元素都是相对较小的元素。
注意,在jdk1.8版本中,PriorityQueue默认是小顶对,可以在构造器传入一个容量和比较器,这个比较器可以使用lamaba表达式来实现,(o1,o2)->o2-o1为大顶堆。
import java.util.*;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> result = new ArrayList<Integer>();
int length = input.length;
if(k > length || k == 0){
return result;
}
//大顶堆:每次出出队列都是最大元素,(o1,o2)->(o2-o1);
PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(k,(o1,o2)->o2-o1);
for (int i = 0; i < length; i++) {
if (maxHeap.size() != k) {
maxHeap.offer(input[i]);
}else{
if(maxHeap.peek() > input[i]){
maxHeap.poll();
maxHeap.offer(input[i]);
}
}
}
for (Integer integer : maxHeap) {
result.add(integer);
}
return result;
}
}
连续子数组最大和
问题描述:
给定一个数组,求出一个连续子数组,使得它的和最大。
解题思路:
- 方法1:
动态规划,设dp[i]表示以i为尾元素的最大和,则动态规划的步骤为:
(1)确定dp[i]表示的意义
(2)确定base_case:dp[0]=array[0];
(3)确定状态:最大和
(4)枚举选择,选择最优的一种:选择1为:dp[i-1]为正数,即dp[i-1]+array[i];选择2为:dp[i-1]为负数,即dp[i]=array[i];最后求最大的max(dp[i-1]+array[i],array[i]);
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int[] dp=new int[array.length];
//base_case;
dp[0]=array[0];
int res=array[0];
for(int i=1;i<array.length;i++){
//做出选择
//选择1:array[i]是正数,添加array[i],则,dp[i]=dp[i-1]+array[i];
//选择2:array[i]是负数;
dp[i]=Math.max(dp[i-1]+array[i],array[i]);
res=Math.max(res,dp[i]);
}
return res;
}
}
- 方法2:
设置一个贡献值,每遍历一个元素,先试探的加上array[i], 如果和为负数,显然,以i结尾的元素对整个结果不作贡献,那设置这个元素的贡献值为0,否则有贡献,加上贡献值。
最后判断整体的贡献值是否为0,如果是,则整个数组中最大的元素为最大子数组的和;
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int res=array[0];
int con=0;//初始化贡献值
for(int i=0;i<array.length;i++){
if(con+array[i]<0){
//负数贡献值为零,不计入
con=0;
}else{
con+=array[i];
res=Math.max(con,res);
}
}
if(con!=0){
//贡献不为0;
return res;
}
//贡献为0
res=array[0];
for(int i=0;i<array.length;i++){
res=Math.max(array[i],res);
}
return res;
}
}
把数组中的数字组合成最小的数
题目描述:
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
解题思路:
本质上是一个排序的过程,只是排序的规则需要改变,例如,对于{a,b}而言,按照题目要求,有两种情况:
(1)“ab”>“ba” ,则排序顺序为:b a
(2)“ab”<“ba”,则排序顺序为:a b
(3) “ab”==“ba”,则排序的顺序任意。
在java中,java.util.Collections工具包有个sort(List,Comparator)方法,可以对list按照传入的Comparator进行比较,我们在这个Comparator中进行上述逻辑的判断;
Comparator的用法:
其内部有个int compare(O1,O2)方法,返回值决定O1和O2如何排序,如果返回<0的数,表明为false,不需要调整原来的顺序,若返回>0的数,需要调整原来的顺序即O2 O1;
直接代码
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Solution {
public String PrintMinNumber(int [] numbers) {
StringBuilder sb=new StringBuilder();
ArrayList<Integer> list=new ArrayList<Integer>();
for(int i=0;i<numbers.length;i++){
list.add(numbers[i]);
}
Collections.sort(list,new Comparator<Integer>(){
@Override
public int compare(Integer o1,Integer o2){
String str1=o1+""+o2;
String str2=o2+""+o1;
//a b
//ab>ba 则应该b a
//ab<ba a b
// ab==ba 0
int res=str1.compareTo(str2);//str1-str2
if(res>0){
return 1;//需要交换a 和 b
}else if(res<0){
return -1;
}else{
return 0;
}
}
});
for(Integer e:list){
sb.append(e);
}
return sb.toString();
}
}
数组中逆序对问题
题目描述
数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
解题思路:
根据归并排序的改进来求解,在归并排序的过程中,两个子数组[a, b,c] [d , e,f],如果a>d,则需要先将d放入缓存数组,同时(a,d)是一个逆序对,且b和c也能形成逆序对。
直接看代码中解释更容易理解
public class Solution {
int res=0;
public int InversePairs(int [] array) {
if(array.length<2){
return 0;
}
int[] tmp=new int[array.length];
mergeSort(array,0,array.length-1,tmp);
return res;
}
//递归划分,到l==r为止(只有一个元素)
private void mergeSort(int[] array,int l,int r,int[] tmp){
if(l>=r){
return ;
}
//折半划分,先求出mid
int mid=(l+r)/2;
mergeSort(array,l,mid,tmp);
mergeSort(array,mid+1,r,tmp);
merge(array,l,mid,r,tmp);
}
private void merge(int[] array,int l,int mid,int r,int[] tmp){
//合并的过程
//[l,mid]和[mid+1,r]
int i=l;
int j=mid+1;
int k=0;
while(i<=mid && j<=r){
if(array[i]>array[j]){
tmp[k++]=array[j++];
//核心部分,思考关键:
//array[i]>array[j]说明了这两个元素是逆序的关系,并且
//[l,mid]和[mid+1,r]已经排好序,即array[mid]>array[l],故array[1...mid]均符合逆序的关系
//数量为mid-i+1
res+=(mid-i+1);
res=res%1000000007;
}else{
tmp[k++]=array[i++];
}
}
//两部分必然有一部分先遍历完成,将剩余的部分复制tmp
while(i<=mid){
tmp[k++]=array[i++];
}
//右半部分没复制完成
while(j<=r){
tmp[k++]=array[j++];
}
//注意不是从0开始,而是从当前处理的子数组的l开始可能是l或者mid+1;
k=0;
int tmpLeft=l;
while(tmpLeft<=r){
array[tmpLeft++]=tmp[k++];
}
}
}
和为Sum的连续正整数序列
题目描述:
找出所有连续的和为Sum的正整数序列,输出结果按从小到大排列
解题思路:
双指针,由于是连续的正整数序列,所以为等差数列,差值为1,故curSum=(
a
0
a_0
a0+
a
n
a_n
an)*n/2;通过比较curSum和Sum的大小来移动窗口。分三种情况:
(1)但curSum==sum,找到,将窗口内的所有数据加入结果集合,同时窗口左移动。
(2)surSum< sum,窗口右边扩大,增加了一个元素,
(3)curSum>sum,左边窗口右移动,减少了一个元素,
import java.util.ArrayList;
public class Solution {
public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
//利用双指针,连续的正整数[1,2,...n]
//初始化窗口[1,2],至少含有两个元素。
ArrayList<ArrayList<Integer>> res=new ArrayList<ArrayList<Integer>>();
int left=1;
int right=2;
while(left<right){
int curSum=(left+right)*(right-left+1)/2;//标准的求和公式
ArrayList<Integer> list=new ArrayList<Integer>();
if(curSum==sum){
//将窗口内的数据全部加入进来
for(int i=left;i<=right;i++){
list.add(i);
}
res.add(list);
left++;
}else if(curSum<sum){
//右边窗口移动
right++;
}else{
left++;
}
}
return res;
}
}