2021年JAVA 面试题之--数据结构篇【附详细答案】

先罗列本篇文章包含的 Java 常见面试的主题:

目录

一、数组

二、字符串

三、栈和队列

  • 链表
  • 二叉树
  • 哈希表
  • 图算法
  • 动态规划
  • 排序算法

项目推荐:

2000多G的计算机各行业电子资源分享(持续更新)

2020年微信小程序全栈项目之喵喵交友【附课件和源码】

Spring Boot开发小而美的个人博客【附课件和源码】

Java微服务实战296集大型视频-谷粒商城【附代码和课件】

Java开发微服务畅购商城实战【全357集大项目】-附代码和课件

最全最详细数据结构与算法视频-【附课件和源码】

在这里插入图片描述

————————————————

2021年JAVA 精心整理的常见面试题-附详细答案

https://mikejun.blog.csdn.net/article/details/114488339

2021年- 精心整理的 SpringMVC 常见面试题-【附详细答案】

https://mikejun.blog.csdn.net/article/details/114992529

2021年JAVA 面试题之--数据结构篇【附详细答案】

https://mikejun.blog.csdn.net/article/details/114647742

三天刷完《剑指OFFER编程题》--Java版本实现(第一天)

https://mikejun.blog.csdn.net/article/details/106996017

三天刷完《剑指OFFER编程题》--Java版本实现(第二天)

https://mikejun.blog.csdn.net/article/details/108098502

三天刷完《剑指OFFER编程题》--Java版本实现(第三天)

https://mikejun.blog.csdn.net/article/details/108253489

一、数组

1. 在java中,声明一个数组过程中,是如何分配内存的?

1.  当声明数组类型变量时,为其分配了(32位)引用空间,由于未赋值,因此并不指向任何对象;

2.  当创建了一个数组对象(也就是new出来的)并将其地址赋值给了变量,其中创建出来的那几个数组元素相当于引用类型变量,因此各自占用(32位的)引用空间并按其默               认初始化规则被赋值为null

3.  程序继续运行,当创建新的对象并(将其地址)赋值给各数组元素,此时堆内存就会有值了

2. 找数组当中重复的元素

第一种方法:两层for循环 0(n^2)

public class TestArray {
	public static void main(String[] args) {
		
		int[] arr = new int[] {1, 2, 5, 5, 6, 6, 7, 2, 9, 2};
		
		findDupicateInArray(arr);
		
	}
	
	//第一种,两层for循环,指定一个循环一周   0(n^2)
	public static void  findDupicateInArray(int[] arr) {
		int count = 0;
		for (int i = 0; i < arr.length; i++) {
			for (int j = i+1; j < arr.length; j++) {
				if (arr[i] == arr [j]) {
					System.out.println("arr["+j+"]="+ arr[i] + "和" + "arr["+i+"]="+arr[i]+"冲突" + ", ");
				}
			}
			
		}
	}
}

第二种方法:扫描的时候用哈希表来存储数组的每一个元素,当碰到数组元素与哈希表中一致时,可以确认数字重复。这样的方法时间复杂度是O(N),但是需要临时空间O(N)。


	public static boolean duplicate(int numbers[],int length,int [] duplication) {
        Map<Integer,Integer> map=new HashMap<>();
        if(length==0) return false;
        for(int i:numbers){
            if(map.containsKey(i)){
               System.out.println("产生冲突的就是"+i);
            } 
            else map.put(i,0);
        }
        duplication[0]=-1;
        return false;
}

3. 翻转一个数组

第一种方法:新开辟一个空间

public class TestArray {
	public static void main(String[] args) {
		
		int[] arr = new int[] {1,2,3,4,5};
		//定义一个新数组,把老数组中的元素反向添加到新数组中
		newArrReverse(arr);
  
		//在本数组上进行翻转
		ArrReverse(arr);
		
		
	}
	//定义一个新数组,把老数组中的元素反向添加到新数组中
	public static void  newArrReverse(int[] arr) {
		int[] brr = new int[arr.length];
		int length = arr.length - 1;  //定义一个从后向前的指针
		for (int i = 0; i < arr.length; i++) {
			brr[i] = arr[length];
			length --;
		}
		System.out.println(Arrays.toString(brr));
	}	
}

第二种方法:交换法。i一直递增,length一直递减,交换

	//在本数组上进行翻转
	public static void ArrReverse(int[] arr) {
		int temp = 0;
		int length = arr.length - 1;  
		for (int i = 0; i < arr.length; i++) {
			if (length == i) {
				break;
			}
			 temp = arr[i];
			 arr[i] = arr[length];
			 arr[length] = temp;
			 length --;
		}
		System.out.println(Arrays.toString(arr));
	}

4. 二维数组中的查找

题目描述

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

示例:

现有矩阵 matrix 如下:

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]


给定 target = 5,返回 true。

给定 target = 20,返回 false。
 

从左下角开始搜索,如果该数大于目标数,则 行数 减去 1,向上搜索小的数值;

如果小于目标数,则 列数 + 1 ,向左边搜索,搜索更大的数值

public class Solution {
    public boolean Find(int target, int [][] array) {
        int row = array.length-1;
        int col = 0;  // 从左下角开始搜索,array.length 表示行的大小,array[0].length表示列的大小
        
        while (row >= 0 && col <= array[0].length-1){
            if (array[row][col] == target){
                return true;
            }else if(array[row][col] > target){
                row--;
            }else{
                col++;
            }
            
        }
        return false;
 
    }
}

5. 旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。


     输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。
     例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
     NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。


二分查找的变形,注意到旋转数组的首元素肯定不小于旋转数组的尾元素,设置中间点。

1. 如果中间点大于首元素,说明最小数字在后面一半,如果中间点小于尾元素,说明最小数字在前一半。依次循环。

2. 当一次循环中首元素小于尾元素,说明最小值就是首元素。

3. 但是当首元素等于尾元素等于中间值,只能在这个区域顺序查找。如: 【1,2,2,3,4】 --> 【2,3,4,1,2】
 

import java.util.ArrayList;
public class Solution {
    public int minNumberInRotateArray(int [] array) {
        int len = array.length;
        if (len == 0){
            return 0;
        }
        
        // 如果第一个数字 都比最后一个数字小,则代表没有旋转
        int left = 0;
        int right = len - 1;
        if (array[left] < array[right]){
            return array[left];
        }
        
        int mid;
        int minVal = array[left];
        while ((right - left) > 1){
            mid = (right + left) / 2;
            if (array[left] <= array[mid]){
                left = mid;
            }else if (array[right] >= array[mid]){
                right = mid;
            }else if ((array[left] == array[mid]) && array[mid] == array[right]){
                // 只能遍历找到最小的值
                for (int i = 0; i < len; i++){
                    if (minVal > array[i]){
                        minVal = array[i];
                        right = i;
                    }
                    
                }
            }
        }
 
        return array[right];
        
    
    }
}

6. 矩形覆盖

我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

比如n=3时,2*3的矩形块有3种覆盖方法:

还是找规律的一道题,归根结底还是 斐波那契数列

public class Solution {
    public int RectCover(int target) {
        if (target <= 0) return 0;
        if (target == 1) return 1;
        if (target == 2) return 2;
        
        return (RectCover(target - 1) + RectCover(target - 2));
 
    }
}

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

题目描述

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

public class Solution {
    public void reOrderArray(int [] array) {
        int index = 0; // 用于保存奇数位置
        int size = array.length;
        
        for (int i =0; i< size; i++){
            if (array[i] % 2 == 1){ // 如果是奇数的话
                int j = i;
                while (j > index){ // 循环交换奇数和偶数位置,偶数位置相对不变
                    int temp = array[j];
                    array[j] = array[j-1];
                    array[j-1] = temp;
                    j--;
                }
            index++; // 更新奇数位置
                
            }
        }
        
    }
}

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

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

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

#使用哈希表   思路: 使用hash,key是数字,value是出现的次数
 

import java.util.Map;
import java.util.HashMap;
 
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        if (array.length == 0) return 0;
        HashMap<Integer, Integer> map = new HashMap();
        for (int i = 0; i < array.length; i++){
            int num = array[i];
            if (map.containsKey(num)){  // 如果原map中key值存在,则 + 1
                map.put(num, map.get(num)+1);
            }else{
                map.put(num, 1);
            }
        }
        for (Map.Entry<Integer, Integer> entry: map.entrySet()){ // 遍历所有的索引和值
            if (entry.getValue() > array.length/2){
                return entry.getKey();
            }
        }
        return 0;
    }
}

 9 把数组排成最小数


输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

 

这里自定义一个比较大小的函数,比较两个字符串s1, s2大小的时候,先将它们拼接起来,比较s1+s2,和s2+s1那个大,如果s1+s2大,那说明s2应该放前面,所以按这个规则,s2就应该排在s1前面。(类似于排序算法)

比如: s1 = 32, s2 = 321, 先将两者字符串转换成数字,比较 s1 + s2 = 32321, 和 s2 + s1 = 32132的数字的大小,

如果前者比较大的话,将s1 和 s2交换。

代码这里使用了一个小技巧: 

int pre = Integer.valueOf(numbers[i] +"" + numbers[j]); // 将数字转换为字符串,拼接,再转换成数字

import java.util.ArrayList;
 
public class Solution {
    public String PrintMinNumber(int [] numbers) {
        
        String res = "";
        for (int i = 0; i < numbers.length; i++){
            for (int j = i+1; j < numbers.length; j++){
                int pre = Integer.valueOf(numbers[i] +"" + numbers[j]); // 将数字转换为字符串,拼接,再转换成数字
                int tail = Integer.valueOf(numbers[j] +"" + numbers[i]);
                if (pre > tail){
                    int temp;
                    temp = numbers[i];
                    numbers[i] = numbers[j];
                    numbers[j] = temp;
                }
            }
        }
        for (int i = 0; i < numbers.length; i++){
            res += numbers[i];
        }
        return res;
 
    }
}

10.丑数


丑数的定义:把只包含质因子2、3和5的数称作丑数(Ugly Number)。

例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

首先从丑数的定义我们知道,一个丑数的因子只有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)丑数数组: 1

乘以2的队列:2

乘以3的队列:3

乘以5的队列:5

选择三个队列头最小的数2加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

(2)丑数数组:1,2

乘以2的队列:4

乘以3的队列:3,6

乘以5的队列:5,10

此时可能出现,乘以5 的数值大于 乘以3 的数值,所以要取 乘以3和乘以5的最小值

选择三个队列头最小的数3加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

(3)丑数数组:1,2,3

乘以2的队列:4,6

乘以3的队列:6,9

乘以5的队列:5,10,15

选择三个队列头里最小的数4加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
 

import java.util.ArrayList;
 
public class Solution {
    public int GetUglyNumber_Solution(int index) {
        
        if (index <= 0) return 0;
        ArrayList<Integer> res = new ArrayList();
        res.add(1);
        int i2=0, i3=0, i5=0;
        
        while (res.size() < index){
            int i2_value = res.get(i2) * 2;
            int i3_value = res.get(i3) * 3;
            int i5_value = res.get(i5) * 5;
            // 找到最小的值
            int minValue = Math.min(i2_value, Math.min(i3_value, i5_value));
            res.add(minValue);
            if (i2_value == minValue) i2++; // 将index往后移动
            if (i3_value == minValue) i3++;
            if (i5_value == minValue) i5++;
        }
        
        return res.get(res.size()-1);
    }
}

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

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

使用哈希表,这是比较容易想到的方法

//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
import java.util.HashMap;
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        int size = array.length;
        HashMap<Integer, Integer> map = new HashMap();
        for (int i = 0; i < size; i++){
            if (!map.containsKey(array[i])){
                map.put(array[i], 1);
            }else{
                map.put(array[i], map.get(array[i]) + 1);
            }
        }
        // 获取value值为 1 的key值
        boolean sign = false;
        for (int i = 0; i < size; i++){
            
            if (map.get(array[i]) == 1){
                if (sign == false){
                    num1[0] = array[i];
                    sign = true;
                }else{
                    num2[0] = array[i];
                }
                
            }
        }
        
    }
}

使用异或

使用的原则是:

位运算中异或的性质:两个相同数字异或=0,一个数和0异或还是它本身。

当只有一个数出现一次时,我们把数组中所有的数,依次异或运算,最后剩下的就是落单的数,因为成对儿出现的都抵消了。

只不过这里使用两次异或,内在还是要将不同的两个数字分开,再异或。

任何一个数字异或他自己都等于0,0异或任何一个数都等于那个数。数组中出了两个数字之外,其他数字都出现两次,那么我们从头到尾依次异或数组中的每个数,那么出现两次的数字都在整个过程中被抵消掉,那两个不同的数字异或的值不为0,也就是说这两个数的异或值中至少某一位为1。

我们找到结果数字中最右边为1的那一位i,然后一次遍历数组中的数字,如果数字的第i位为1,则数字分到第一组,数字的第i位不为1,则数字分到第二组。这样任何两个相同的数字就分到了一组,而两个不同的数字在第i位必然一个为1一个不为1而分到不同的组,然后再对两个组依次进行异或操作,最后每一组得到的结果对应的就是两个只出现一次的数字。
 

//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        int size = array.length;
        if (size == 0) return ;
        int num = 0;
        for (int i = 0; i < size; i++){
            num ^= array[i];
        }
        int index = getFirstBitOne(num);
        for (int i = 0; i< size; i++){
            if(isBitOne(array[i], index)){
                num1[0] ^= array[i];
            }else{
                num2[0] ^= array[i];
            }
        }
    }
    
    private int getFirstBitOne(int num){
        int index = 0;
        while ((num & 1) == 0 && index < 32){
            num >>= 1;
            index += 1;
        }
        return index;
    }
    
    private boolean isBitOne(int num, int index){
        // 判断num在第index位是否为 1
        return (((num >> index)& 1) == 1);
    }
    
}

12.数组中重复的数字


在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。

例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

使用类似哈希表的方法:
 

public class Solution {
    // Parameters:
    //    numbers:     an array of integers
    //    length:      the length of array numbers
    //    duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;
    //                  Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++
    //    这里要特别注意~返回任意重复的一个,赋值duplication[0]
    // Return value:       true if the input is valid, and there are some duplications in the array number
    //                     otherwise false
    public boolean duplicate(int numbers[],int length,int [] duplication) {
        //由于: 一个长度为n的数组里的所有数字都在0到n-1的范围内
        boolean[] sign = new boolean[length];
        
        for (int i = 0; i < length; i++){
            if (sign[numbers[i]] == true){
                duplication[0] = numbers[i];
                return true;
            }
            sign[numbers[i]] = true;
        }
        return false;
    }
}

二、字符串

1.将字符串转换成整数

将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。

输入描述:

输入一个字符串,包括数字字母符号,可以为空
输出描述:

如果是合法的数值表达则返回该数字,否则返回0
示例1

输入

+2147483647
1a33

输出

2147483647
0

public class Solution {
    public int StrToInt(String str) {
        int size = str.length();
        if (str.equals("+") && size==1) return 0;
        if (str.equals("-") && size == 1) return 0;
        if (size == 0) return 0;
        
        int sum = 0;
        for (int i = 0; i < size; i++){
            if (str.charAt(i) == '-'){
                continue;
            }else if (str.charAt(i) == '+'){
                continue;
            }else if (str.charAt(i) >= '0' && str.charAt(i) <= '9'){
                sum = sum * 10 + str.charAt(i) - '0';
            }else{
                return 0;
            }
        }
        return str.charAt(0) == '-' ? -sum : sum; // 判断第一个符号是否是 + -
    }
}

2.表示数值的字符串

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。

例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。

但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。

需要注意的是,指数E后面必须跟一个整数,不能没有数,也不能为小数。
 

public class Solution {
    public boolean isNumeric(char[] str) {
        int size = str.length;
        if (size == 0) return false;
        
        boolean hasE = false, hasDot = false, hasSign = false;
        for (int i = 0; i < size; i++){
            // 如果当前字符是 e, E
            if (str[i] == 'e' || str[i] == 'E'){
                // 1.首部出现,2.尾部出现,3.重复出现,均不是
                if (i == 0) return false;
                if (i == size-1) return false;
                if (hasE) return false;
                hasE = true;
                // 如果当前字符是 +, -
            }else if (str[i] == '+' || str[i] == '-'){
                // 1.如果当前不是第一位,且前一位不是E, e
                // 2.如果已经出现过+-, 且前一位不是 E, e,均为false
                if(i != 0 && str[i-1] != 'E' && str[i-1] != 'e') return false;
                if (hasSign && str[i-1] != 'E' && str[i-1] != 'e') return false;
                hasSign = true;
            }else if (str[i] == '.'){
                // 1.存在多个 . 2. 在 E 后面
                if (hasDot) return false;
                if (hasE) return false;
                hasDot = true;
            }else if (str[i] < '0' || str[i] > '9'){
                return false;
            }
        }
        return true;
    }
}

3.字符流中第一个不重复的元素

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。

输出描述:

如果当前字符流没有存在出现一次的字符,返回#字符。

public class Solution {
    //Insert one char from stringstream
    String str = "";
    char[] hash = new char[256];
    public void Insert(char ch)
    {
        str += ch;  // 拼接字符串
        hash[ch] += 1; // 对字符 计数
    }
  //return the first appearence once char in current stringstream
    public char FirstAppearingOnce()
    {
        for (int i = 0; i < str.length(); i++){
            char ch = str.charAt(i);
            if (hash[ch] == 1){
                return ch;
            }
        }
        return '#';
    }
}

4.字符串的排列

输入一个字符串,按字典序打印出该字符串中字符的所有排列。

例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

输入描述:

输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。
递归法,问题转换为先固定第一个字符,求剩余字符的排列;求剩余字符排列时跟原问题一样。

递归算法实现:

(1) 遍历出所有可能出现在第一个位置的字符(即:依次将第一个字符同后面所有字符交换);

(2) 固定第一个字符,求后面字符的排列(即:在第1步的遍历过程中,插入递归进行实现)。

以"abc"为例

1.第一次进到这里是ch=['a','b','c'],list=[],i=0,我称为 状态A ,即初始状态

 那么j=0,swap(ch,0,0),就是['a','b','c'],进入递归,自己调自己,只是i为1,交换(0,0)位置之后的状态我称为 状态B 

 i不等于2,来到这里,j=1,执行第一个swap(ch,1,1),这个状态我称为 状态C1 ,再进入递归函数,此时标记为T1,i为2,那么这时就进入上一个if,将"abc"放进list中

 -------》此时结果集为["abc"]

2.执行完list.add之后,遇到return,回退到T1处,接下来执行第二个swap(ch,1,1),状态C1又恢复为状态B

恢复完之后,继续执行for循环,此时j=2,那么swap(ch,1,2),得到"acb",这个状态我称为C2,然后执行递归函数,此时标记为T2,发现i+1=2,所以也被添加进结果集,此时return回退到T2处往下执行

-------》此时结果集为["abc","acb"]

然后执行第二个swap(ch,1,2),状态C2回归状态B,然后状态B的for循环退出回到状态A

             

            //             a|b|c(状态A)

            //               |

            //               |swap(0,0)

            //               |

            //             a|b|c(状态B)

            //             /  \

            //   swap(1,1)/    \swap(1,2)  (状态C1和状态C2)

            //           /      \

            //         a|b|c   a|c|b

             

3.回到状态A之后,继续for循环,j=1,即swap(ch,0,1),即"bac",这个状态可以再次叫做状态A,下面的步骤同上

-------》此时结果集为["abc","acb","bac","bca"]      

            //             a|b|c(状态A)

            //               |

            //               |swap(0,1)

            //               |

            //             b|a|c(状态B)

            //             /  \

            //   swap(1,1)/    \swap(1,2)  (状态C1和状态C2)

            //           /      \

            //         b|a|c   b|c|a

             

4.再继续for循环,j=2,即swap(ch,0,2),即"cab",这个状态可以再次叫做状态A,下面的步骤同上

-------》此时结果集为["abc","acb","bac","bca","cab","cba"]

             

            //             a|b|c(状态A)

            //               |

            //               |swap(0,2)

            //               |

            //             c|b|a(状态B)

            //             /  \

            //   swap(1,1)/    \swap(1,2)  (状态C1和状态C2)

            //           /      \

            //         c|b|a   c|a|b

5.最后退出for循环,结束。

import java.util.ArrayList;
import java.util.List;
import java.util.Collections;
public class Solution {
    public ArrayList<String> Permutation(String str) {
        List<String> list = new ArrayList();
        if (str.length() == 0) return (ArrayList)list;
        
        permutationHelper(str.toCharArray(), 0, list); // 递归实现
        Collections.sort(list);
        return (ArrayList)list;
    }
    
    private void permutationHelper(char[] cs, int i, List<String> list){
        if (i == cs.length-1){  // 如果索引是最后一位的话
            String s = String.valueOf(cs);
            if (!list.contains(s)){  // 防止有重复的字符串,如输入为 aab时
                list.add(s);
                return;
            }
        }else{
            for (int j = i; j < cs.length; j++){
                swap(cs, i, j);
                permutationHelper(cs, i+1, list);
                swap(cs, i, j);  // 回溯法,恢复之前字符串顺序,达到第一位依次跟其他位交换的目的
            }
        }
    }
    
    private void swap(char[] cs, int i, int j){  // 交换两个字符
        char temp;  
        temp = cs[i];
        cs[i] = cs[j];
        cs[j] = temp;
    }
}

5.第一次只出现一次的字符


在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)

使用哈希解题,遍历字符串,哈希key值为遍历的单个字符,value为出现次数;最后重新遍历,找到一开始value为1的key值

import java.util.HashMap;
 
public class Solution {
    public int FirstNotRepeatingChar(String str) {
        int size = str.length();
        if (size == 0) return -1;
        
        HashMap<Character, Integer> map = new HashMap();
        for (int i = 0; i < size; i++){
            char value = str.charAt(i);
            if (!map.containsKey(value)){  // 如果key 值不存在,则新建key值,value为1
                map.put(value, 1);
            }else{
                map.put(value, map.get(value)+1); // value 值加一
            }
        }
        
        for (int i=0; i < size; i++){
            if (map.get(str.charAt(i)) == 1){
                return i;
            }
        }
        
        return -1;
    }
}

另外一种比较巧妙的方法:
主要还是hash,利用每个字母的ASCII码作hash来作为数组的index。首先用一个58长度的数组来存储每个字母出现的次数,为什么是58呢,主要是由于A-Z对应的ASCII码为65-90,a-z对应的ASCII码值为97-122,而每个字母的index=int(word)-65,比如g=103-65=38,而数组中具体记录的内容是该字母出现的次数,最终遍历一遍字符串,找出第一个数组内容为1的字母就可以了,时间复杂度为O(n)
 

public class Solution {
    public int FirstNotRepeatingChar(String str) {
        int size = str.length();
        if (size == 0) return -1;
        int[] array = new int[58];
        
        for (int i = 0; i< size; i++){
            array[(int)str.charAt(i)-65]++; 
        }
        
        for (int i= 0; i < size; i++){
            if (array[(int)str.charAt(i)-65] == 1){
                return i;
            }
        }
        
        
        return -1;
    }
}

6. 左旋转字符串


汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。

例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!

解题思路:


原理:YX = (X^T Y ^T) ^T,初始看这种想法还是蛮有技巧的

假设输入str为abcXYZdef,n=3   

反转前3个字符,得到cbaXYZdef

反转第n个字符后面所有的字符cbafedZYX

反转整个字符串XYZdefabc

public class Solution {
    public String LeftRotateString(String str,int n) {
        int size = str.length();
        if (size == 0 || n < 0) return "";
        n = n % size;
        
        char[] strChar = str.toCharArray();
        reverse(strChar, 0, n-1); // 前n 个字符反转
        reverse(strChar, n, size-1); // n个字符后反转
        reverse(strChar, 0, size-1); // 将反转了的字符串再反转
        return new String(strChar);
 
    }
    // 二分法将数组反转
    private void reverse(char[] strChar, int start, int end){
        while (start < end){
            char temp = strChar[start];
            strChar[start] = strChar[end];
            strChar[end] = temp;
            start += 1;
            end -= 1;
        }
    }
}

7.翻转单词序列

牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。

例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?

 

1.将 字符串转换成字符串数组

2. 新建一个空间

3. 从字符串数组尾部开始遍历

4.将其字符存放到新的空间,并每次增加空格

public class Solution {
    public String ReverseSentence(String str) {
        if (str.trim().equals("")) return str;
        String[] s = str.split(" ");
        StringBuffer sb = new StringBuffer();
        
        for (int i = s.length-1; i >= 0; i--){
            sb.append(s[i]);
            if (i > 0){
                sb.append(" ");
            }
        }
        return sb.toString();
    }
}

8.扑克牌的顺子

LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子.....LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。

先置换特殊字符AJQK为数字,排序,然后求出大小王即0的个数,然后求出除去0之外的,数组间的数字间隔(求间隔的时候记得减去1,比如4和5的间隔为5-4-1,表示4和5是连续的数字),同时求间隔的时候需要鉴别是否出现对。最后比较0的个数和间隔的大小即可。

# 如果出现相同的数,则必定不是顺子

1、排序 

2、计算所有相邻数字间隔总数 

3、计算0的个数 

4、如果2、3相等,就是顺子 

5、如果出现对子,则不是顺子

import java.util.Arrays;
public class Solution {
    public boolean isContinuous(int [] numbers) {
        int size = numbers.length;
        if (size < 5) return false;
        
        int oneNums = 0;
        int intervalNums = 0;
        Arrays.sort(numbers); // 对数组排序
        for (int i = 0; i < size-1; i++){
            
            if (numbers[i] == 0){
                oneNums += 1;
                continue;
            }
            if (numbers[i] == numbers[i+1]) return false; // 此行代码要放在==0判断后面,防止有2个0
            intervalNums += numbers[i+1] - numbers[i] - 1;
        }
        if (oneNums >= intervalNums) return true;
        return false;
 
    }
}

三、栈和队列

1. 请实现一个栈

public class Stack {

    public Node head;
    public Node current;


    //方法:入栈操作
    public void push(int data) {
        if (head == null) {
            head = new Node(data);
            current = head;
        } else {
            Node node = new Node(data);
            node.pre = current;//current结点将作为当前结点的前驱结点
            current = node;  //让current结点永远指向新添加的那个结点
        }
    }

    public Node pop() {
        if (current == null) {
            return null;
        }

        Node node = current; // current结点是我们要出栈的结点
        current = current.pre;  //每出栈一个结点后,current后退一位
        return node;

    }


    class Node {
        int data;
        Node pre;  //我们需要知道当前结点的前一个结点

        public Node(int data) {
            this.data = data;
        }
    }


    public static void main(String[] args) {

        Stack stack = new Stack();
        stack.push(1);
        stack.push(2);
        stack.push(3);

        System.out.println(stack.pop().data);
        System.out.println(stack.pop().data);
        System.out.println(stack.pop().data);
    }

}

2. 请实现一个队列

  队列的创建有两种形式:基于数组结构实现(顺序队列)、基于链表结构实现(链式队列)。

  我们接下来通过链表的形式来创建队列,这样的话,队列在扩充时会比较方便。队列在出队时,从头结点head开始。

public class Queue {
    public Node head;
    public Node curent;

    //方法:链表中添加结点
    public void add(int data) {
        if (head == null) {
            head = new Node(data);
            curent = head;
        } else {
            curent.next = new Node(data);
            curent = curent.next;
        }
    }

    //方法:出队操作
    public int pop() throws Exception {
        if (head == null) {
            throw new Exception("队列为空");
        }

        Node node = head;  //node结点就是我们要出队的结点
        head = head.next; //出队之后,head指针向下移

        return node.data;

    }


    class Node {
        int data;
        Node next;

        public Node(int data) {
            this.data = data;
        }
    }


    public static void main(String[] args) throws Exception {
        Queue queue = new Queue();
        //入队操作
        for (int i = 0; i < 5; i++) {
            queue.add(i);
        }

        //出队操作
        System.out.println(queue.pop());
        System.out.println(queue.pop());
        System.out.println(queue.pop());

    }
}

3. 2 个队列实现一个栈

入栈时把数据入到有数据的那个队列中
出栈时把有数据的队列中的数据除过最后一个,其他都入队到另外一个队列,最后一个就是要出栈的元素

/**
 *  实现两个队列实现一个栈
 */
public class StackQueue1 {
    private LinkedList<Integer> list1;
    private LinkedList<Integer> list2;
    public StackQueue1() {
        list1 = new LinkedList<>();
        list2 = new LinkedList<>();
    }

    /**
     * 入栈
     */
    public void push(int data) {
        if(!list1.isEmpty()) {
            list1.offer(data);
        } else {
            //默认栈为空的时候都入栈到list2
            list2.offer(data);
        }
    }

    /**
     * 出栈
     */
    public Integer pop() {
        if(isEmpty()) {
            return null;
        }
        //srcList表示有数据的队列,destList表示没有数据的队列
        LinkedList<Integer> srcList = list1;
        LinkedList<Integer> destList = list2;
        if(list1.isEmpty()) {
            srcList = list2;
            destList = list1;
        }
        Integer poll = null;
        while(!srcList.isEmpty()) {
            poll = srcList.poll();
            //如果是最后一次出队,那么不再把最后一个元素入队到另一个队列中
            if(srcList.isEmpty()) {
                break;
            }
            destList.offer(poll);
        }
        return poll;
    }

    /**
     * 判断栈空
     * @return
     */
    private boolean isEmpty() {
        return list1.isEmpty() && list2.isEmpty();
    }
}

4. 两个栈实现一个队列

/**
 *  两个栈实现一个队列
 */
public class StackQueue2 {
    private LinkedList<Integer> srcList;
    private LinkedList<Integer> destList;

    public StackQueue2() {
        srcList = new LinkedList<>();
        destList = new LinkedList<>();
    }

    /**
     * 入队
     */
    public void offer(int data) {
        srcList.push(data);
    }

    /**
     * 出队
     */
    public Integer poll() {
        if(isEmpty()) {
            return null;
        }
        if(destList.isEmpty()) {
            //把srcList中的数据都搬过来
            while(!srcList.isEmpty()) {
                Integer pop = srcList.pop();
                destList.push(pop);
            }
        }
        return destList.pop();
    }

    /**
     * 判断队列是否为空
     * @return
     */
    private boolean isEmpty() {
        return srcList.isEmpty() && destList.isEmpty();
    }
}

5. 包含min函数的栈

题目描述

定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。

思路:利用一个辅助栈来存放最小值

    栈  3,4,2,5,1

    辅助栈 3,3,2,2,1

每入栈一次,就与辅助栈顶比较大小,如果小就入栈,如果大就入栈当前的辅助栈顶

当出栈时,辅助栈也要出栈

这种做法可以保证辅助栈顶一定都当前栈的最小值
 

import java.util.Stack;
 
public class Solution {
 
    private Stack<Integer> dataStack = new Stack();
    private Stack<Integer> minStack = new Stack();
    
    public void push(int node) {
        dataStack.push(node);
        if (minStack.isEmpty() || min() > node){ // 如果为空,则之间push进去,如果最小栈的最小值都比node大,也把node值push
            minStack.push(node); 
        }else{
            minStack.push(min());
        }
    }
    
    public void pop() {
        dataStack.pop();
        minStack.pop();
    }
    
    public int top() {
        return dataStack.peek();
    }
    
    public int min() {
        return minStack.peek();
    }
}

6. 栈的压入弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)

1. 建立一个辅助栈,把push序列的数字依次压入辅助栈,

2. 每次压入后,比较辅助栈的栈顶元素和pop序列的首元素是否相等,相等的话就推出pop序列的首元素和辅助栈的栈顶元素,

3.若最后辅助栈为空,则push序列可以对应于pop序列。

举例:

入栈1,2,3,4,5

出栈4,5,3,2,1

首先1入辅助栈,此时栈顶1≠4,继续入栈2

此时栈顶2≠4,继续入栈3

此时栈顶3≠4,继续入栈4

此时栈顶4=4,出栈4,弹出序列向后一位,此时为5,,辅助栈里面是1,2,3

此时栈顶3≠5,继续入栈5

此时栈顶5=5,出栈5,弹出序列向后一位,此时为3,,辅助栈里面是1,2,3

import java.util.ArrayList;
import java.util.Stack;
 
public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
      if (pushA.length==0 || popA.length == 0){
          return false;
      }
        // 使用一个辅助栈
        Stack<Integer> stack = new Stack();
        int index = 0;
        for (int i=0; i< pushA.length; i++){
            stack.push(pushA[i]);
            while (!stack.isEmpty() && stack.peek()==popA[index]){
                stack.pop();
                index++;  // 用于数组后一位继续判断
            }
        }
        return stack.isEmpty();
    }
}

7.从上往下打印二叉树

相当于时树的层次遍历

从上往下打印出二叉树的每个节点,同层节点从左至右打印。(依次打印出每层的结点值)

需要一个队列和一个存储数据的数组

import java.util.ArrayList;
import java.util.LinkedList;
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
        // 使用队列的方法
        ArrayList<Integer> res = new ArrayList();
        if (root == null){
            return res;
        }
        
        LinkedList<TreeNode> queue = new LinkedList();
        queue.offer(root);
        
        while (!queue.isEmpty()){
            TreeNode node = queue.poll();
            res.add(node.val);
            if (node.left != null){
                queue.offer(node.left);
            }
            if (node.right != null){
                queue.offer(node.right);
            }
        }
        return res;
 
    }
}

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
面试讲解视频,不是网盘,下载既有视频,屡试不爽的面试宝典。 Java面试题01.面试的整体流程 Java面试题02.java的垮平台原理 Java面试题03.搭建一个java的开发环境 Java面试题04.java中int占几个字节 Java面试题05.java面向对象的特征 Java面试题06.装箱和拆箱 Java面试题07.==和equals的区别 Java面试题08.String Java面试题09.讲一下java中的集合 Java面试题10.ArrayList 和LinkedList的区别 Java面试题11.HashMap和HashTable的区别 Java面试题12.实现一个拷贝文件的工具类要使用字节流还是字符串 Java面试题13.线程的的实现方式?怎么启动线程?怎么区分线程? Java面试题14.线程并发库和线程池的作用 Java面试题15.设计模式和常用的设计模式 Java面试题16.http get post请求的区别 Java面试题17.说说你对Servlet的理解 Java面试题18.Servlet的生命周期 Java面试题19.Servlet中forward和redirect的区别 Java面试题20.jsp和Servlet的相同点和不同点 Java面试题21.内置对象和四大作用域和页面传值 Java面试题22.Session和Cookie的区别和使用场景 Java面试题23.mvc模式和mvc各部分的实现 Java面试题24.数据库分类和常用数据库 Java面试题25.关系型数据库的三范式 Java面试题26.事务的四大特征 Java面试题27.mysql数据库最大连接数 Java面试题28.mysql和oracle的分页语句 Java面试题29.触发器的使用场景? Java面试题30.存储过程的优点 Java面试题31.jdbc调用存储过程 Java面试题32.简单说一下你对jdbc的理解 Java面试题33.写一个jdbc的访问oracle的列子 Java面试题34.jdbc中preparedStatement比Statement的好处 Java面试题35.数据库连接池的作用 Java面试题36.HTML Java面试题37.简单介绍了一下Ajax Java面试题38.js和JQuery的关系 Java面试题39.jQuery中的常用选择器 Java面试题40.jQuery中页面加载完毕事件 Java面试题41.jQuery中Ajax和原生js实现Ajax的关系 Java面试题42.简单说一下html5 Java面试题43.简单说一下css3 Java面试题44.bootstrap的是什么 Java面试题45.什么是框架 Java面试题46.简单介绍一下MVC模式 Java面试题47.简单说一下对mvc框架的理解 Java面试题48.struts2的执行流程或者struts2的原理 Java面试题49.Struts2的拦截器是什么 Java面试题50.Spring MVC的执行流程 Java面试题51.SpringMVC和Struts2的不同 Java面试题52.简单介绍一下Spring或者Spring的两大核心 Java面试题53.AOP是什么?都用它做什么? Java面试题54.Spring事务的传播特性和隔离级别 Java面试题55.ORM是什么?ORM框架是什么? Java面试题56.ibatis和hibernate有什么不同 Java面试题57.hibernate对象状态及其转换 Java面试题58:hibernate的缓存 Java面试题59.webservice的使用场景 Java面试题60.activiti的简单介绍 Java面试题61.linux的使用场景 Java面试题62.linux常用命令 Java面试题63:怎么操作linux服务器 Java面试题64:有没有使用过云主机 Java面试题65:数据库优化方面的事情 Java面试题66:如果查询和定位慢查询 Java面试题67:数据库优化之数据库表设计遵循范式 Java面试题68:选择合适的数据库引擎 Java面试题69:选择合适的索引 Java面试题70:使用索引的一些技巧 Java面试题71:数据库优化之分表 Java面试题72:数据库的读写分离 Java面试题73:数据库优化之缓存 Java面试题74:sql语句优化小技巧 Java面试题75:批量插入几百万条数据 Java面试题76:有没有使用过redis Java面试题77:redis的使用场景 Java面试题78:redis存储对象的方式 Java面试题79:redis数据淘汰机制 Java面试题80:java访问redis级redis集群 ......
java面试题真的很多,下面我来回答一个有关多线程的问题。 在Java中实现多线程有两种方式,一种是继承Thread类,另一种是实现Runnable接口。这两种方式有何区别? 继承Thread类的方式是直接定义一个类继承Thread,并重写它的run()方法。然后创建该类的对象,并调用对象的start()方法来启动线程。这种方式简单直接,但因为Java是单继承的,所以如果某个类已经继承了其他类,就不能再直接继承Thread类实现多线程。 实现Runnable接口的方式是定义一个类实现Runnable接口,并实现其唯一的抽象方法run()。然后创建Thread类的对象,将实现了Runnable的对象作为参数传递给Thread类的构造方法。最后调用Thread对象的start()方法来启动线程。这种方式灵活性更大,因为Java允许一个类实现多个接口,所以即使某个类已经继承了其他类,仍然可以通过实现Runnable接口来实现多线程。 另一个区别在于资源共享的问题。继承Thread类的方式,不管是数据还是方法,都是线程自己拥有的,不存在共享的情况。而实现Runnable接口的方式,多个线程可以共享同一个对象的数据和方法,因为多个线程共同操作的是同一个Runnable对象。 总结来说,继承Thread类方式简单直接,但只能通过单继承来实现多线程;实现Runnable接口方式更灵活,可以解决单继承的限制,并且多个线程可以共享同一个Runnable对象的数据和方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值