(一)剑指offer 数组篇

1.二维数组中的查找

题目

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

答案

这道题的考察点也就是在一个二维数组中查找指定的数是否存在。
可以从数组的左下角来看,向右递增,向上递减,所以就可以从左下角开始查找,当要查的数比左下角小时上移,大时右移。

package jzOffer;

public class Solution {
	public static void main(String[] args) {
		int[][] array = {
				{1, 3, 5, 7, 9 },
				{2, 4, 6, 8, 10}
		};
		boolean result = Find(11, array);
		System.out.println(result);
	}
	
	public static boolean Find(int target, int [][] array) {
		int lenX = array.length;
		int lenY = array[0].length;
		for(int i = lenX - 1, j =  0; i >= 0 && j < lenY; ) {
			if(array[i][j] > target) {
				i--;
			}else if (array[i][j] < target) {
				j++;
			}else {
				return true;
			}
		}
		return false;
    }
}

2.数组中重复的数字

题目

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2

答案

(1)用额外空间的方法

	/**
	 * 利用hashmap,很简单
	 * @param numbers
	 * @param length
	 * @param duplication
	 * @return
	 */
	public boolean duplicate1(int numbers[],int length,int [] duplication) {
		HashMap<Integer, Integer> map =  new HashMap<>();
		for(int i = 0; i < length; i++) {
			if (map.containsKey(numbers[i])) {
				duplication[0] = numbers[i];
				return true;
			}
			map.put(numbers[i], 0);
		}
		return false;
    }

(2)重排此数组(与平常的排序不一样)
思路:比较好的思路的分析为,数组中的数字为0到n-1的范围内。如果这个数组中没有重复的数字,则对应的i位置的数据也为i。可以重排此数组
在这里插入图片描述

	/**
	 * 重排法
	 * @param numbers
	 * @param length
	 * @param duplication
	 * @return
	 */
	public boolean duplicate2(int numbers[],int length,int [] duplication) {
		int i = 0;
		int temp = 0;
		while(i < length) {
			if(numbers[i] == i) {
				i++;
			}else {
				temp = numbers[i];
				if (numbers[i] == numbers[temp]) {
					duplication[0] = numbers[i];
					return true;
				}
				numbers[i] = numbers[temp];
				numbers[temp] = temp;
			}
		}
		return false;
	}

3.构建乘积数组

题目

给定一个数组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]。不能使用除法。

答案

思路:构建出一个矩阵的样子,但是不用计算举证的方法,还是用原来的方法,只不过分两个for()来计算。
在这里插入图片描述

	/**
	 * 构建乘积数组
	 * @param A
	 * @return 构建后的数组
	 */
	 public int[] multiply(int[] A) {
		 int n = A.length;
		 int[] B = new int[n];
		 B[0] = 1;
		 for(int i = 1; i < n; i++) {
			 B[i] = B[i - 1] * A[i - 1];
		 }
		 int temp = 1;
		 for(int i = n - 1; i >= 0; i-- ) {
			 B[i] = temp * B[i];
			 temp = temp * A[i];
		 }
		return B; 
	 }

4.数组中只出现一次的数字

题目

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

答案

思路:利用异或操作。同时还要注意:移位、取反等位运算。
在这里插入图片描述

	 /**
	  * 数组中只出现一次的数组
	  * @param array
	  * @param num1,num2 分别为长度为1的数组。传出参数
	  *    将num1[0],num2[0]设置为返回结果
	  */
	 public static void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
		 
		 int n1 = 0;
		 int temp = 0;
	     for (int j : array)
			temp ^= j;
	    
	    //这知识点我原来不知道,还用取余,取商,循环判断,多蠢!!!
	     int sign = temp & (~temp + 1);//求得二进制中第一位1,比如101和011得到010

	     for (int i : array) {
			if((sign & i) != 0)
				n1 ^= i;
		}
	     num1[0] = n1;
	     num2[0] = n1 ^ temp;
	     
	   }

5.和为S的两个数

题目

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

答案

思路:在数组中找到两个数,与S比较,如果相等我们就找到了;如果比S小,我们就想办法让这两个数的和变大一点;如果比S大,我们就希望让两个数的和变小一点。

由于数组中的数是递增排好序的,我们可以定义两个指针,left指向最左边,right指向最右边,用leftright所指的数的和与S比较:如果“和”小于S,就让left右移;如果“和”大于S,就让right左移;直到找到与S相等的值或者left == right

题目中说了:输出的两个输的乘积最小。
由于数组时有序数组,所以说:
在这里插入图片描述

/**
 * 	和为S的两个数
 * @param array
 * @param sum
 * @return
 */
public static ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
	int left = 0;
	int right = array.length - 1;
	ArrayList<Integer> list = new ArrayList<>();
	while(left < right) {
		int small = array[left];
		int big = array[right];
		if(small + big == sum) {
			list.add(small);
			list.add(big);
			return list;
		}else if(small + big < sum) {
			left++;
		}else {
			right--;
		}
	}
	return list;
    }

6.和为S的正数序列

题目

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!

输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序

答案
  • 思路:可以模仿上一题的方式,定义两个smallbig指针用来滑动。
    首先让small指向 1 ,big指向 2
    然后:
    求出small -> big的序列和curSum,包括smallbig
    如果curSum == S,保存这个序列,让big右移,寻找下一个序列。
    如果curSum > S,让small左移,减小curSum的值。
    如果curSum < S,让big右移,增大粗人Sum的值。
    循环上面步骤,直到small的值大于S值的一半。
/**
 * 	和为S的正数序列
 * @param sum
 * @return 
 */
public static ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
	ArrayList<ArrayList<Integer>> result = new ArrayList<>();
	ArrayList<Integer> alist = new ArrayList<Integer>();
	int small = 1;
	int big = 2;
	while(small <= (sum + 1)/2) {
		int curSum = 0;
		for(int i = small; i <= big; i++)
			curSum += i;
		if(sum == curSum) {
			for(int i = small; i <= big; i++)
				alist.add(i);
			result.add(new ArrayList<Integer>(alist));
			alist.clear();
			big++;
		}else if(curSum > sum) {
			small++;
		}else {
			big++;
		}	
	}
	return result;
	
}

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

题目

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

答案

思路:
一次在数组中删除两个不同的数,最后剩下的数 有可能就是是超过一半的。所以要检验一下。 一个数出现次数大于一半,他肯定会被剩下来,但是剩下来的缺不一定满足。

算法步骤:
如果times为0,就把候选设为当前值。
如果下个数和候选一样,times就++。
如果下个数和候选不一样,times就–。相当于对子,同归于尽。因为超过一半的数肯定超过剩下的所有数。所以和这个数对,这个数肯定会剩下来。
但是剩下的数不一定是,比如 1 2 3 剩下3 比如 1 2 1 3 3 3 2 2 也是剩下3.所以要余外的判断,看是否这个数真的超过。

public int MoreThanHalfNum_Solution(int [] array) {
		int num = 0;
		int times = 0;
		for (int i : array) {
			if(times == 0) {
				num = i;
				times = 1;
			}else if(num == i) {
				times++;
			}else {
				times--;
			}
		}
		times = 0;
		for (int i : array) {
			if(num == i) 
				times++;
			if(times > array.length/2)
				return num;
		}
		return 0;	
	}

8.连续子数组的最大和

题目

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

答案

思路:
不是看当前的值,而是看当前的累计值,
如果当前累计为负数,那么加上现在这个数,肯定和就小了,
所以重新从当前值开始累计
如果为正,那就继续加,
但是要时刻保存下最大值来,因为后面的累计有可能小。

	public int FindGreatestSumOfSubArray1(int[] array) {  
	  if(array.length == 0)
	   return 0;
	  int total = array[0];		//当前累计和
	  int maxsum = array[0];	//当前最大累计和
	  for (int i = 1; i < array.length; i++) {
	   if(total >= 0)		//只要total的值大于零,对于后面的数来说,它就有相加的意义。
	    total = total + array[i];
	   else
	    total = array[i];
	   if(total>maxsum)
	    maxsum = total;
	  }
	  return maxsum;
	 }

9.调整数组的顺序使奇数位于偶数前面

题目

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

答案

(1)使用额外空间,很简单,没什么好说的

public static void reOrderArray(int [] array) {
        ArrayList<Integer> jList = new ArrayList<>();
        ArrayList<Integer> oList = new ArrayList<>();
        for (int i : array) {
			if(i % 2 == 1) 
				jList.add(i);
			else 
				oList.add(i);
		}
        jList.addAll(oList);
        
        int i = 0;
        for (Integer j : jList) {
			array[i++] = j.intValue();
		}
        
    }

(2)使用滑动变量
思路:
设置两个滑动变量start和end
让strat从数组的最左边开始向右滑动,遇到第一个偶数停止,并指向它
让end从strat+1开始向右滑动,遇到第一个奇数位停止,并指向它
然后,将数组的start -> end - 1这段值整体向右移动一位,再把end所指的值放到空出来的那个位置。
注意移位的时候,要把将要覆盖的那个位置的值提前取出来放到一个中间变量里面:

int temp =  array[end];
  	for(int i = end; i > strat; i--)
       array[i] = array[i - 1];
    array[strat] = temp;

代码如下:

 public void reOrderArray(int [] array) {
    	int strat = 0;
    	int end = 0;
    	int len = array.length;
    	while(strat < len) {
    		while(strat < len && (array[strat] & 1) == 1)
    			strat++;
    		end = strat + 1;
    		while(end < len && (array[end] & 1) == 0)
    			end++;
    		if(end < len ) { 
    			int temp =  array[end];
    			for(int i = end; i > strat; i--)
       			 array[i] = array[i - 1];
       		array[strat] = temp;
    		}
    		else {
    			break;
    		}
    	}
    }

10. 把数组排成最小的数

题目

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

答案

对于给定的2个数,a和b。如何确定两者之间的排序策略呢?我们可以发现这两者的排列为:ab,ba。我们最终目的是找到字典序最小的那个排列,所以我们肯定应该保持这种关系,从而决定是否交换顺序:

  1. 当ab < ba, a排在b的左边
  2. 当ab > ba, b排在a的左边
  3. 当ab = ba, 位置关系随意
 /**
     * 	把数组排成最小的数
     * @param numbers
     * @return
     */
    public static String PrintMinNumber(int [] numbers) {
    	int len = numbers.length;
    	String[] str = new String[len];
    	StringBuffer buf = new StringBuffer();
    	for(int i = 0; i < len; i++) {
    		str[i] = String.valueOf(numbers[i]);
    	}
		Arrays.sort(str, new Comparator<String>() {
			@Override
			public int compare(String o1, String o2) {
				return (o1 + o2).compareTo(o2 + o1);
			}
		});
    	for (String st : str)
    		buf.append(st);
    	return buf.toString();
    }

11.数组中的逆序对

题目

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

输入描述:

题目保证输入的数组中没有的相同的数字
数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5

示例1

输入 :
1,2,3,4,5,6,7,0
输出 :
7

答案

思路:用了归并的思想,也就是变相的写一个归并排序。

不知道归并看这里:https://blog.csdn.net/qq_42570601/article/details/97147158
下面和一般的归并实现不同的地方在于,一般咋们使用归并的时候,往新数组中存放时是从头开始,也就是++,这里是从尾开始,用的--

然后要注意的是:这段代码的意义!

			if (leftA[i] > rightA[j]) {
				count = count + j + 1;
				count %= 1000000007;
				newA[k--] = leftA[i--];
			} else {
				newA[k--] = rightA[j--];
			}

如果说左边的数组是1,4,8,右边的数组是2,5,6
当8 > 6 时,就有三个逆序对。
当4 > 2 时,就有一个逆序对。

代码如下:

	/**
	 * 
	 * @param array
	 * @return
	 */
	int count = 0;

	public int InversePairs(int[] array) {
		if (array == null || array.length == 0)
			return 0;
		mergeSort(array, 0, array.length - 1);
		return count;
	}

	public  int[] mergeSort(int[] array, int start, int end) {
		if (start == end)
			return new int[] { array[start] };
		int mid = (start + end) / 2;
		int[] leftA = mergeSort(array, start, mid);
		int[] rightA = mergeSort(array, mid + 1, end);

		int i = leftA.length - 1;
		int j = rightA.length - 1;
		int[] newA = new int[i + j + 2];
		int k = newA.length - 1;

		while (i >= 0 && j >= 0)
			if (leftA[i] > rightA[j]) {
				count = count + j + 1;
				count %= 1000000007;
				newA[k--] = leftA[i--];
			} else {
				newA[k--] = rightA[j--];
			}
		while (i >= 0)
			newA[k--] = leftA[i--];
		while (j >= 0)
			newA[k--] = rightA[j--];

		return newA;
	}

12.数字在排序数组中出现的次数

题目

统计一个数字在排序数组中出现的次数。

答案

思路:题目中说了,已经排好序,二分法一半是跑不了的。

可以让一个方法采用二分查找,先找到一个与k值想等的位置,再判断k的前一个位置对应的值是否为k,如果不是,则返回这个位置的下标(就是第一个出现的位置),如果是,则抛弃后半段,再从前半段中继续查找。注意巧妙的避免数组下标越界!!!

同样的方式找到最后一次出现的位置,就很容易得到想要的结果。

	/**
	 * 数字在排序数组中出现的次数
	 * @param array
	 * @param k
	 * @return
	 */
	public static int GetNumberOfK(int[] array, int k) {
		return getLast(array, k) - getFrist(array, k) + 1;
	}
	//找到 K 第一次出现的位置
	public static int getFrist(int num[], int k) {
		int left = 0;
		int right = num.length - 1;
		while (left <= right) {
			int mid = (left + right) / 2;
			if (k < num[mid]) {
				right = mid - 1;
			} else if (k > num[mid]) {
				left = mid + 1;
			} else {
				if (mid == 0 || num[mid - 1] != k) {
					return mid;
				} else {
					right = mid - 1;
				}
			}
		}
		return 0;
	}
	//找到 K 最后一次出现的位置
	public static int getLast(int num[], int k) {
		int left = 0;
		int right = num.length - 1;
		while (left <= right) {
			int mid = (left + right) / 2;
			if (k < num[mid]) {
				right = mid - 1;
			} else if (k > num[mid]) {
				left = mid + 1;
			} else {
				if (mid == num.length - 1 || num[mid + 1] != k) {
					return mid;
				} else {
					left = mid + 1;
				}
			}
		}
		return -1;
	}
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yelvens

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值