剑指offer(31-40)
31、整数1出现的次数(从1到n整数中1出现的次数)
题目:求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
思路:定义一个计数变量sum,将整数1-n转换为字符串,然后for循环遍历字符串中的每个字符,当字符满足判断条件,即字符等于1时,sum++。
public class Solution {
public int NumberOf1Between1AndN_Solution(int n) {
int sum=0;
for(int i=1;i<=n;i++){
String str=String.valueOf(i);
for(int j=0;j<str.length();j++){
if(str.charAt(j)=='1'){
sum++;
}
}
}
return sum;
}
}
32、把数组排成最小的数
题目:输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
思路:将数组中所有的数字的类型全部转为String类型,用数组str存起来。接下来为数组中的字符串排序,排序规则为:将两个字符串拼接,s1+s2和s2+s1,比较拼接的两个字符串的大小(按字典序),如"3"和"32",拼接后为"332"和"323",而"323"小于"332",此时"32"将会被排到"3"前面,两个数比较之后,我们得到了这两个数排列顺序的局部最优,如果所有数都按照这种方法排列,那么数组的排序就按照我们想要的方式达到了全局最优。排列完毕后只需要按顺序将所有的字符串拼接返回即可。
代码:
import java.util.*;
import java.util.ArrayList;
public class Solution {
public String PrintMinNumber(int [] numbers) {
if(numbers.length==0||numbers==null){
return "";
}
List<Integer> list=new ArrayList<Integer>();
for(int i=0;i<numbers.length;i++){
list.add(numbers[i]);
}
list.sort((a,b)->(a+""+b).compareTo(b+""+a));
StringBuffer stringBuffer=new StringBuffer();
for(int a:list){
stringBuffer.append(a);
}
return stringBuffer.toString();
}
}
33、丑数
题目:把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
思路:来源于牛客网事无巨细,悉究本末的思路,根据丑数的定义,链接:https://www.nowcoder.com/questionTerminal/6aa9e04fc3794f68acf8778237ba065b
一个丑数的因子只有2,3,5,那么丑数p = 2 ^ x * 3 ^ y * 5 ^ z,换句话说一个丑数一定由另一个丑数乘以2或者乘以3或者乘以5得到,那么我们从1开始乘以2,3,5,就得到2,3,5三个丑数,在从这三个丑数出发乘以2,3,5就得到4,6,10,6,9,15,10,15,25九个丑数,我们发现这种方得到重复的丑数,而且我们题目要求第N个丑数,这样的方法得到的丑数也是无序的。那么我们可以维护三个队列:
- 丑数数组:1
乘以2的队列:2
乘以3的队列:3
乘以5的队列:5
选择三个队列头最小的数2加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
- 丑数数组:1,2
乘以2的队列:4
乘以3的队列:3,6
乘以5的队列:5,10
选择三个队列头最小的数3加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列
- 丑数数组:1,2,3
乘以2的队列:4,6
乘以3的队列:6,9
乘以5的队列:5,10,15
选择三个队列头里最小的数4加入丑数数组,同时将该最小的数乘以**2,3,5放入三个队列;
- 丑数数组:1,2,3,4
乘以2的队列:6,8
乘以3的队列:6,9,12
乘以5的队列:5,10,15,20
选择三个队列头里最小的数5加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
- 丑数数组:1,2,3,4,5
乘以2的队列:6,8,10,
乘以3的队列:6,9,12,15
乘以5的队列:10,15,20,25
选择三个队列头里最小的数6加入丑数数组,但我们发现,有两个队列头都为6,所以我们弹出两个队列头,同时将12,18,30放入三个队列
……………………
import java.lang.Math;
public class Solution {
public int GetUglyNumber_Solution(int index) {
if(index<=0){
return 0;
}
int p2=0,p3=0,p5=0;
int[] result=new int[index];
result[0]=1;
for(int i=1;i<index;i++){
result[i]=Math.min(result[p2]*2,Math.min(result[p3]*3,result[p5]*5));
if(result[i]==result[p2]*2) p2++;
if(result[i]==result[p3]*3) p3++;
if(result[i]==result[p5]*5) p5++;
}
return result[index-1];
}
}
34、第一个只出现一次的字符位置
题目:在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).(从0开始计数)
思路:这道题可以Map方法来解决,我们首次对字符串数组中的字符进行遍历,以<元素值,次数>的形式存储在map中,存储的过程如下
遍历字符串数组中的每一个元素
判断map集合中是否存在该元素,若不存在则新添加<元素,1>存放在键值对中
若存在,则新添加<元素,map.get(array[i])+1>存放在map中
最后,我们只需循环判断map.get(array[i]) == 1就可获得第一个只出现一次的字符位置
代码
import java.util.HashMap;
import java.util.Map;
public class Solution {
public int FirstNotRepeatingChar(String str){
if(str.length()==0||str==null){
return -1;
}
Map<Character,Integer> map = new HashMap<Character,Integer>();
char[] array = str.toCharArray();
for(int i=0; i < str.length(); i++ ){
if(!map.containsKey(array[i])){
map.put(array[i],1);
}else {
map.put(array[i], map.get(array[i])+1);
}
}
//System.out.println(map.toString());
for(int i = 0; i<str.length();i++){
if(map.get(array[i]) == 1) {
return i;
}
}
return -1;
}
}
35、数组中的逆序对
题目:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数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
思路:这道题有两种方法,一种方法是循环匹配法,一种方法是归并排序法
- 循环匹配法
这种方法是扫描整个数组,逐个比较和它后面的数字大小,如果后面的数字比它小,则这两个数字就组成了一个逆序对,假设数组中含有n个数字。由于每个数字都要和O(n)这个数字比较,因此这个算法的时间复杂度为O(n^2)。但是这方法不能在有限的时间内完成代码的运行,故舍弃
- 归并排序法
参考牛客网一叶浮沉的思路,使用归并排序算法,在归并排序的过程中,后一个数组的数如小于前一个数组的数,则一定能够构成逆序对且逆序对的数目可计算,因为待归并的两个数组提前已经归并排序过,所以不会出现像前面那样少统计或者多统计的情况出现。
循环匹配法代码,不能在有限的时间内完成
public class Solution {
public int InversePairs(int [] array) {
if(array.length<=1){
return 0;
}
int p=0;
for(int i=0;i<array.length-1;i++){
for(int j=i+1;j<array.length;j++){
if(array[i]>array[j]){
p++;
}
}
}
if(p>0){
return p%1000000007;
}
return 0;
}
}
归并排序
public class Solution {
private int cnt;
private void MergeSort(int[] array, int start, int end){
if(start>=end)return;
int mid = (start+end)/2;
MergeSort(array, start, mid);
MergeSort(array, mid+1, end);
MergeOne(array, start, mid, end);
}
private void MergeOne(int[] array, int start, int mid, int end){
int[] temp = new int[end-start+1];
int k=0,i=start,j=mid+1;
while(i<=mid && j<= end){
//如果前面的元素小于后面的不能构成逆序对
if(array[i] <= array[j])
temp[k++] = array[i++];
else{
//如果前面的元素大于后面的,那么在前面元素之后的元素都能和后面的元素构成逆序对
temp[k++] = array[j++];
cnt = (cnt + (mid-i+1))%1000000007;
}
}
while(i<= mid)
temp[k++] = array[i++];
while(j<=end)
temp[k++] = array[j++];
for(int l=0; l<k; l++){
array[start+l] = temp[l];
}
}
public int InversePairs(int [] array) {
MergeSort(array, 0, array.length-1);
return cnt;
}
}
36、两个链表的第一个公共结点
题目:输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
思路:这道题有两种思路解决
- 补全链表的长度
补全链表的长度,使得两个链表长度相等,这样就可以使用双向指针分别遍历两个链表,直到找到两个链表的第一个公共结点。
- 循环遍历
带以两个指针p1、p2分别指向两个链表的头结点,为了保证长度一致,当一个链表遍历结束之后就从另一个链表的头结点开始遍历,这两个指针会总会在某一个时刻相遇。
循环遍历代码:
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead1==null||pHead2==null){
return null;
}
ListNode p1=pHead1;
ListNode p2=pHead2;
while(p1!=p2){
p1=p1.next;
p2=p2.next;
if(p1!=p2){
if(p1==null) p1=pHead2;
if(p2==null) p2=pHead1;
}
}
return p1;
}
}
37、数字在升序数组中出现的次数
题目:统计一个数字在排序数组中出现的次数。
思路:两种思路,①直接遍历查找②考虑到数组有序,可以使用二分查找
普通遍历查找
public class Solution {
public int GetNumberOfK(int [] array , int k) {
if(array.length==0){
return 0;
}
int count=0;
for(int i=0;i<array.length;i++){
if(array[i]==k){
count++;
}
}
return count;
}
}
二分查找
import java.util.Arrays;
public class Solution {
public int GetNumberOfK(int [] array , int k) {
int index=Arrays.binarySearch(array,k);
if(index<0){
return 0;
}
int count=1;
for(int i=index+1;i<array.length;i++){
if(array[i]==k){
count++;
}
}
for(int i=0;i<index;i++){
if(array[i]==k){
count++;
}
}
return count;
}
}
38、二叉树的深度
题目:输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
思路:这道题直接使用递归,返回左/右子树中最大的深度。
- F,左右子结点均为null,返回1
- D,左结点+1=2
- B,右子结点1,选取,左子结点+1=3,返回给A
- A,右子结点为1,选取,左子结点+1=4,返回
代码
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
*/
public class Solution {
public int TreeDepth(TreeNode root) {
if(root==null){
return 0;
}
int nleft=TreeDepth(root.left);
int nright=TreeDepth(root.right);
return 1+(nleft>nright?nleft:nright);
}
}
39、平衡二叉树
题目: 输入一棵二叉树,判断该二叉树是否是平衡二叉树。在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树
思路:关于树的算法题,一般都可以使用递归来实现,平衡二叉树的条件是**|左子树高度-右子树高度|<1**,我们利用这个条件可以分别计算出左右子树的高度。
代码
import java.lang.Math;
public class Solution {
private boolean isBalanced=true;
public boolean IsBalanced_Solution(TreeNode root) {
//平衡二叉树的左右子树深度<=1
getTreeDepth(root);
return isBalanced;
}
public int getTreeDepth(TreeNode node){
if(node==null){
return 0;
}
int nleft=getTreeDepth(node.left);
int nright=getTreeDepth(node.right);
if(Math.abs(nleft-nright)>1){
isBalanced=false;
}
return nright>nleft?nright+1:nleft+1;
}
}
40、数组中只出现一次的数字
题目:一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字
思路:这道题最简单的思路就是用哈希查找
- 哈希查找
遍历数组,以数组中的值为key、出现次数为value构建Map,构建完成后,查询map获取出现次数为1为1的key值分别放入num1[0]和num2[0]
代码实现
//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class Solution {
public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
Map<Integer,Integer> map=new HashMap<>();
for(int i=0;i<array.length;i++){
if(map.containsKey(array[i])){
map.put(array[i],2);
}else{
map.put(array[i],1);
}
}
//遍历map数组,查询
for(int i=0;i<array.length;i++){
if(map.get(array[i])==1){
if(num1[0]==0){
num1[0]=array[i];
}else{
num2[0]=array[i];
}
}
}
}
}