学习随笔简介

跟随着黑马满老师的《Java八股文面试题视频教程,Java面试八股文宝典》学习,视频教程地址:Java八股文面试题视频教程,Java面试八股文宝典(含阿里、腾迅大厂java面试真题,java数据结构,java并发,jvm等最新java面试真题)_哔哩哔哩_bilibili

共分为四个部分,分别是 基础篇、并发篇、虚拟机、框架篇 

本篇更新基础篇的内容(基础篇已经完结)

目录

学习随笔简介

一、二分查找

1.二分查找的实现

2.整数溢出问题

3.相关面试题(选择)--解题的办法

二、冒泡排序

1.冒泡排序的实现

三、选择排序

1.选择排序的实现

四、冒泡排序与选择排序的比较

1.区别

2.稳定排序与不稳定排序

五、插入排序

1.插入排序的实现

六、选择排序与插入排序的比较

1.区别

七、希尔排序

1.希尔排序的实现

八、插入和选择--推导某一轮排序结果

九、快速排序

1.算法描述

2.单边循环快排(lomuto 洛穆托分区方案)

3.双边循环快排

4.快速排序的特点

5.洛穆托分区方案 vs 霍尔分区方案

十、ArrayList--扩容规则

十一、lterator_FailFast 和 lterator_FailSafe

1.基本介绍

2.两者区别(代码演示)

十二、ArrayList 和 LinkedList

1.两者对比

十三、HashMap

1.基本数据结构

2.树化与退化

3.索引计算

4.put与扩容

5.并发问题

6.key的设计

十四、单例模式

1.单例模式之饿汉式

2. 单例模式之枚举饿汉式

3.单例模式之懒汉式

4.单例模式之双检索懒汉式

5.单例模式之内部类

6.JDK中单例的实现


一、二分查找

1.二分查找的实现

基本步骤:

  1. 前提:有已排序数组 A(假设已经做好)

  2. 定义左边界 L、右边界 R,确定搜索范围,循环执行二分查找(3、4两步)

  3. 获取中间索引 M = Floor((L+R) /2)

  4. 中间索引的值 A[M] 与待搜索的值 T 进行比较

    ① A[M] == T 表示找到,返回中间索引

    ② A[M] > T,中间值右侧的其它元素都大于 T,无需比较,中间索引左边去找,M - 1 设置为右边界,重新查找

    ③ A[M] < T,中间值左侧的其它元素都小于 T,无需比较,中间索引右边去找, M + 1 设置为左边界,重新查找

  5. 当 L > R 时,表示没有找到,应结束循环

代码实现:

public class BinarySearch {public static void main(String[] args) {int[] array = {1,5,8,11,19,22,31,35,40,45,48,49,50}; //已排序的数组int target = 48; //需要搜索的目标值int index = binarySearch(array, target); //元素对应的索引System.out.println(index);}//二分查找,找到返回元素索引,找不到返回 -1public static int binarySearch(int[] array, int target){//定义左边界 leftint left = 0;//定义右边界 rightint right = array.length - 1;//定义中间的索引int middle;while (left <= right) {//表示左边界还未超过右边界,重复执行 3、4 两步middle = (left + right)/2;//获得中间索引if (array[middle] == target) { //找到目标值return middle;}else if (array[middle] > target){ //表示中间值middle右边的均大于目标值,舍弃right = middle - 1;//重新定义右边界}else { // 中间值 middle 左边的均小于目标值 ,舍弃left = middle + 1;//重新定义左边界}}return -1;}}

运行结果:

10

2.整数溢出问题

上述代码虽然实现了二分查找算法 ,但是在取中间值的时候超过整数的最大值,就会出现整数溢出的问题,所以需要对上述代码进行优化,从而解决整数溢出的问题

方法1:在取中间值时,对原本的公式进行公式演化,可以避免造成整数溢出的问题

演化过程:

        middle = (left + right) / 2 = left/2 + right/2 = left - left/2 + right/2 = left+(right-left)/2

优化代码: 

int left = 0;
int right = Integer.MAX_VALUE - 1;//int middle = (left + right) / 2;
//方法1.进行公式演化,可以避免造成整数溢出的问题
// -- left/2 + right/2 -- left - left/2 + right/2 -- left+(right-left)/2
int middle = left + (right - left) / 2;
System.out.println(middle);//此时假设在右侧
left = middle + 1;
middle = left + (right - left) / 2;
System.out.println(middle);

方法2:同样是在取中间值的时候,利用无符号的右移运算,也可以解决整数溢出的问题,而且也比除法的效率高

优化代码:

int left = 0;
int right = Integer.MAX_VALUE - 1;//方法2.利用无符号的右移运算,也可以解决整数溢出的问题,而且也比除法的效率高
int middle = (left + right) >>> 1;
System.out.println(middle);//此时假设在右侧
left = middle + 1;
middle = (left + right) >>> 1;
System.out.println(middle);

3.相关面试题(选择)--解题的办法

  1. 京东实习生招聘)有一个有序表为 1,5,8,11,19,22,31,35,40,45,48,49,50 当二分查找值为 48 的结点时,查找成功需要比较的次数   --- 4次

  2. (美团点评校招)使用二分法在序列 1,4,6,7,15,33,39,50,64,78,75,81,89,96 中查找元素 81 时,需要经过( )次比较   --- 4次

  3. (北京易道博识校招)在拥有128个元素的数组中二分查找一个数,需要比较的次数最多不超过多少次  --- 7次

解题办法:

1、2  ---  奇数二分取中间,偶数二分取中间靠左

3 ---  2ⁿ = N  =  log₂N  = log₁₀N / log₁₀2 (其中 n 为查找次数,N 为元素个数 )

                结果为整数--即为最终结果;结果为小数,则舍去小数部分,整数加一为最终结果

【注意】

        1.上述写的二分查找是以 jdk 中 Arrays.binarySearch 的实现作为示范

        2.但实际上,二分查找有诸多变体,一旦使用变体的实现代码,则左右边界的选取会有变化,进而会影响之前选择题的答案选择

二、冒泡排序

1.冒泡排序的实现

基本步骤:

  1. 依次比较数组中相邻两个元素大小,若 a[j] > a[j+1],则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后

  2. 重复以上步骤,直到整个数组有序

代码实现:

public class BubbleSort {public static void main(String[] args) {int[] array = {5,9,7,4,1,3,2,8};bubble(array);}public static void bubble(int[] array){for (int j = 0; j < array.length - 1; j++) {//一轮冒泡boolean swapped = false; //表示是否发生了交换 false表示没有发生交换for (int i = 0; i < array.length - 1 - j; i++) {if (array[i] > array[i+1]) {swap(array, i, i+1);swapped = true;}}System.out.println("第"+(j+1)+"轮冒泡:"+ Arrays.toString(array));if (!swapped) {//数组中的元素没有发生交换,直接退出循环break;}}}public static void swap(int[] array,int i,int j){//将数组中下标为i和j的元素分别交换位置int t = array[i]; //先将下标为 i 的元素取出来放进缓存区array[i] = array[j];//将下标为 j 的元素放到原先下标为 i 的位置array[j] = t; //将下标为 i 的元素 从缓存区里取出来放入 原先下标为 j 的位置}}

运行结果:

第1轮冒泡:[5, 7, 4, 1, 3, 2, 8, 9]
第2轮冒泡:[5, 4, 1, 3, 2, 7, 8, 9]
第3轮冒泡:[4, 1, 3, 2, 5, 7, 8, 9]
第4轮冒泡:[1, 3, 2, 4, 5, 7, 8, 9]
第5轮冒泡:[1, 2, 3, 4, 5, 7, 8, 9]
第6轮冒泡:[1, 2, 3, 4, 5, 7, 8, 9]

上述代码的实现相较普通的冒泡排序优化在

  • 每经过一轮冒泡,内层循环就可以减少一次
  • 如果某一轮冒泡没有发生交换,则表示所有的数据有序,可以结束外层循环

进行再一步优化:

public static void bubble_v2(int[] array){int n = array.length - 1;while (true) {int lastIndex = 0;//定义在一轮冒泡中最后发生交换的下标for (int i = 0; i < n; i++) {if (array[i] > array[i+1]) {swap(array, i, i+1);lastIndex = i;//取交换前的 前一个元素的下标}}n = lastIndex; //将 lastIndex 作为 新一轮冒泡的循环次数System.out.println(Arrays.toString(array));if (n == 0) {//循环次数为零,退出循环break;}}}

运行结果:

[5, 7, 4, 1, 3, 2, 8, 9]
[5, 4, 1, 3, 2, 7, 8, 9]
[4, 1, 3, 2, 5, 7, 8, 9]
[1, 3, 2, 4, 5, 7, 8, 9]
[1, 2, 3, 4, 5, 7, 8, 9]
[1, 2, 3, 4, 5, 7, 8, 9]

相较上一个冒泡算法的实现,本次实现的优化点在   每轮冒泡时,最后一次交换索引可以作为下一轮冒泡的比较次数,如果这个值为零,表示整个数组有序,直接退出外层循环即可

三、选择排序

1.选择排序的实现

 基本步骤:

  1. 将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选出最小的元素,放入排序子集

  2. 重复以上步骤,直到整个数组有序

代码实现:

public class SelectionSort {public static void main(String[] args) {int[] array = {5,3,7,2,1,9,8,4};selection(array);}private static void selection(int[] array){for (int i = 0; i < array.length - 1; i++) {//i 代表每轮选择最小元素要交换到的目标索引int minIndex = i; // 代表最小元素的索引for (int j = minIndex + 1; j < array.length; j++) { //假定下标为 0 的元素为最小,所以循环次数为数组的长度if (array[minIndex] > array[j]) {minIndex = j; //更新最小下标}}if (minIndex != i) {  //如果最小下标发生变化,则更换元素的位置swap(array, minIndex, i);}System.out.println(Arrays.toString(array));}    }public static void swap(int[] array,int i,int j){//将数组中下标为i和j的元素分别交换位置int t = array[i]; //先将下标为 i 的元素取出来放进缓存区array[i] = array[j];//将下标为 j 的元素放到原先下标为 i 的位置array[j] = t; //将下标为 i 的元素 从缓存区里取出来放入 原先下标为 j 的位置}}

运行结果:

[1, 3, 7, 2, 5, 9, 8, 4]
[1, 2, 7, 3, 5, 9, 8, 4]
[1, 2, 3, 7, 5, 9, 8, 4]
[1, 2, 3, 4, 5, 9, 8, 7]
[1, 2, 3, 4, 5, 9, 8, 7]
[1, 2, 3, 4, 5, 7, 8, 9]
[1, 2, 3, 4, 5, 7, 8, 9]

四、冒泡排序与选择排序的比较

1.区别

  1. 二者平均时间复杂度都是 O(n²)

  2. 选择排序一般要快于冒泡,因为其交换次数少

  3. 但如果集合有序度高,冒泡优于选择

  4. 冒泡属于稳定排序算法,而选择属于不稳定排序

    • 稳定排序指,按对象中不同字段进行多次排序,不会打乱同值元素的顺序

    • 不稳定排序则反之

2.稳定排序与不稳定排序

分别使用 冒泡排序与选择排序 对扑克牌的花色和数字进行排序(先对花色进行排序,再对数字进行排序)

代码实现:

public class StableVsUnstable {public static void main(String[] args) {System.out.println("=================不稳定================");Card[] cards = getStaticCards();System.out.println(Arrays.toString(cards));selection(cards, Comparator.comparingInt((Card a) -> a.sharpOrder).reversed());System.out.println(Arrays.toString(cards));selection(cards, Comparator.comparingInt((Card a) -> a.numberOrder).reversed());System.out.println(Arrays.toString(cards));System.out.println("=================稳定=================");cards = getStaticCards();System.out.println(Arrays.toString(cards));bubble(cards, Comparator.comparingInt((Card a) -> a.sharpOrder).reversed());System.out.println(Arrays.toString(cards));bubble(cards, Comparator.comparingInt((Card a) -> a.numberOrder).reversed());System.out.println(Arrays.toString(cards));}public static void bubble(Card[] a, Comparator<Card> comparator) {int n = a.length - 1;while (true) {int last = 0; // 表示最后一次交换索引位置for (int i = 0; i < n; i++) {if (comparator.compare(a[i], a[i + 1]) > 0) {swap(a, i, i + 1);last = i;}}n = last;if (n == 0) {break;}}}private static void selection(Card[] a, Comparator<Card> comparator) {for (int i = 0; i < a.length - 1; i++) {// i 代表每轮选择最小元素要交换到的目标索引int s = i; // 代表最小元素的索引for (int j = s + 1; j < a.length; j++) {if (comparator.compare(a[s], a[j]) > 0) {s = j;}}if (s != i) {swap(a, s, i);}}}public static void swap(Card[] a, int i, int j) {Card t = a[i];a[i] = a[j];a[j] = t;}enum Sharp {diamond, club, heart, spade, black, red}static Card[] getStaticCards() {List<Card> list = new ArrayList<>();Card[] copy = Arrays.copyOfRange(Card.cards, 2, 13 * 4 + 2);list.add(copy[7]);list.add(copy[12]);list.add(copy[12+13]);list.add(copy[10]);list.add(copy[9]);list.add(copy[9+13]);return list.toArray(new Card[0]);}static Map<String, Integer> map = Map.ofEntries(Map.entry("RJ", 16),Map.entry("BJ", 15),Map.entry("A", 14),Map.entry("K", 13),Map.entry("Q", 12),Map.entry("J", 11),Map.entry("10", 10),Map.entry("9", 9),Map.entry("8", 8),Map.entry("7", 7),Map.entry("6", 6),Map.entry("5", 5),Map.entry("4", 4),Map.entry("3", 3),Map.entry("2", 2));static class Card {private Sharp sharp;private final String number;private final int numberOrder;private final int sharpOrder;public Card(Sharp sharp, String number) {this.sharp = sharp;this.number = number;this.numberOrder = map.get(number);this.sharpOrder = sharp.ordinal();}private static final Card[] cards;static {cards = new Card[54];Sharp[] sharps = {Sharp.spade, Sharp.heart, Sharp.club, Sharp.diamond};String[] numbers = {"A", "K", "Q", "J", "10", "9", "8", "7", "6", "5", "4", "3", "2"};int idx = 2;for (Sharp sharp : sharps) {for (String number : numbers) {cards[idx++] = new Card(sharp, number);}}cards[0] = new Card(Sharp.red, "RJ");cards[1] = new Card(Sharp.black, "BJ");}@Overridepublic String toString() {return switch (sharp) {case heart -> "[\033[31m" + "♥" + number + "\033[0m]";case diamond -> "[\033[31m" + "♦" + number + "\033[0m]";case spade -> "[\033[30m" + "♠" + number + "\033[0m]";case club -> "[\033[30m" + "♣" + number + "\033[0m]";case red -> "[\033[31m" + "\uD83C\uDFAD" + "\033[0m]";case black -> "[\033[30m" + "\uD83C\uDFAD" + "\033[0m]";};}}}

运行结果:

=================不稳定================
[[♠7], [♠2], [♥2], [♠4], [♠5], [♥5]]
[[♠7], [♠2], [♠4], [♠5], [♥2], [♥5]]
[[♠7], [♠5], [♥5], [♠4], [♥2], [♠2]]
=================稳定=================
[[♠7], [♠2], [♥2], [♠4], [♠5], [♥5]]
[[♠7], [♠2], [♠4], [♠5], [♥2], [♥5]]
[[♠7], [♠5], [♥5], [♠4], [♠2], [♥2]]

由上述结果可以看出:

  • 不稳定排序算法在进行数字排序的时候,会打乱原本同值的花色顺序(原来 ♠2 在前 ♥2 在后,按数字再排后,他俩的位置变了 ) 
  • 稳定排序算法按数字排序的时候,会保留原本同值的花色顺序( ♠2 与 ♥2 的相对位置不变 )

五、插入排序

1.插入排序的实现

基本步骤:

  1. 将数组分为两个区域,排序区域和未排序区域,每一轮从未排序区域中取出第一个元素,插入到排序区域(需保证顺序)

  2. 重复以上步骤,直到整个数组有序

代码实现:

public class InsertSort {public static void main(String[] args) {int[] array = {9,3,7,2,5,8,1,4};insert(array);}private static void insert(int[] array){for (int i = 0; i < array.length; i++) {//i 表示待插入元素的索引int t = array[i]; //表示待插入的元素值int j = i - 1; //表示已将排序区域的元素索引while (j >= 0) {if (t < array[j]) {//待插入的元素值 小于 已将排序区域的元素索引的值array[j+1] = array[j];//将下标为 j 的元素向后移动一位}else { //待插入的元素值 大于 已经排序区域的元素索引的值break; //找到插入位置,直接退出循环}j--; //依次向前进行比较,直到下标出现为负,退出循环}array[j+1] = t;//将待插入的元素值 插入到 空出的位置System.out.println(Arrays.toString(array));}}
}

 运行结果:

[9, 3, 7, 2, 5, 8, 1, 4]
[3, 9, 7, 2, 5, 8, 1, 4]
[3, 7, 9, 2, 5, 8, 1, 4]
[2, 3, 7, 9, 5, 8, 1, 4]
[2, 3, 5, 7, 9, 8, 1, 4]
[2, 3, 5, 7, 8, 9, 1, 4]
[1, 2, 3, 5, 7, 8, 9, 4]
[1, 2, 3, 4, 5, 7, 8, 9]

六、选择排序与插入排序的比较

1.区别

  1. 二者平均时间复杂度都是 O(n²)

  2. 大部分情况下,插入都略优于选择

  3. 有序集合插入的时间复杂度为 O(n)

  4. 插入属于稳定排序算法,而选择属于不稳定排序

七、希尔排序

1.希尔排序的实现

基本步骤: 

  1. 首先选取一个间隙序列,如 (n/2,n/4 … 1),n 为数组长度

  2. 每一轮将间隙相等的元素视为一组,对组内元素进行插入排序,目的有二

    ① 少量元素插入排序速度很快

    ② 让组内值较大的元素更快地移动到后方

  3. 当间隙逐渐减少,直至为 1 时,即可完成排序

代码实现:

public class ShellSort {public static void main(String[] args) {int[] array = {9,3,7,2,5,8,1,4};shell(array);}private static void shell(int[] array){int n = array.length;for (int gap = n / 2; gap > 0; gap /= 2) { //gap 表示元素间的间隙for (int i = gap; i < n; i++) { //i 表示待插入元素的索引int t = array[i];//表示待插入的元素值int j = i;while (j >= gap) {//每次与上一个间隙为 gap 的元素进行插入排序if (t < array[j - gap]) { //array[j-gap] 是上一个元素,如果上一个元素大于待插入的元素值,则让它后移array[j] = array[j - gap];j -= gap;  //下标移到 j-gap 的位置}else { // 如果上一个元素小于等于待插入的元素值,则表示 j 就是插入位置break;}}array[i] = t;System.out.println(Arrays.toString(array)+"gap:" + gap);}}}

运行结果:

[9, 3, 7, 2, 5, 8, 1, 4]gap:4
[9, 3, 7, 2, 5, 8, 1, 4]gap:4
[9, 3, 7, 2, 5, 8, 1, 4]gap:4
[9, 3, 7, 2, 5, 8, 1, 4]gap:4
[9, 3, 7, 2, 5, 8, 1, 4]gap:2
[9, 3, 7, 2, 5, 8, 1, 4]gap:2
[9, 3, 9, 2, 5, 8, 1, 4]gap:2
[9, 3, 9, 2, 5, 8, 1, 4]gap:2
[9, 3, 9, 2, 9, 8, 1, 4]gap:2
[9, 3, 9, 2, 9, 8, 1, 4]gap:2
[9, 3, 9, 2, 9, 8, 1, 4]gap:1
[9, 3, 9, 2, 9, 8, 1, 4]gap:1
[9, 9, 3, 2, 9, 8, 1, 4]gap:1
[9, 9, 3, 2, 9, 8, 1, 4]gap:1
[9, 9, 3, 2, 9, 8, 1, 4]gap:1
[9, 9, 9, 3, 2, 9, 1, 4]gap:1
[9, 9, 9, 3, 2, 9, 1, 4]gap:1

八、插入和选择--推导某一轮排序结果

题目:

使用直接插入排序算法对序列18,23,19,9,23,15进行排序,第三趟排序后的结果为()

A.9,18,15,23,19,23        B.18,23,19,9,23,15

C.18,19,23,9,23,15        D.9,18,19,23,23,15

推算过程:18,23,19,9,23,15       18,19,23,9,23,15      9,18,19,23,23,15

使用直接选择排序算法对序列18,23,19,9,23,15进行排序,第三趟排序后的结果为()

A.9,23,19,18,23,15        B.9,15,18,19,23,23

C.18,19,23,9,23,15        D.18,19,23,9,15,23

推算过程:

        9,23,19,18,23,15  9,15,19,18,23,23  9,15,18,19,23,23

九、快速排序

1.算法描述

  1. 每一轮排序选择一个基准点(pivot)进行分区

    1. 让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区

    2. 当分区完成时,基准点元素的位置就是其最终位置

  2. 在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 (divide-and-conquer)

  3. 从以上描述可以看出,一个关键在于分区算法,常见的有洛穆托分区方案、双边循环分区方案、霍尔分区方案

2.单边循环快排(lomuto 洛穆托分区方案) 

基本步骤: 

  1. 选择最右元素作为基准点元素

  2. j 指针负责找到比基准点小的元素,一旦找到则与 i 进行交换

  3. i 指针维护小于基准点元素的边界,也是每次交换的目标索引

  4. 最后基准点与 i 交换,i 即为分区位置

代码实现:

public class QuickSort1 {public static void main(String[] args) {int[] array = {5,3,7,2,9,8,1,4};//3,5,7,2,9,8,1,4  3,2,7,5,9,8,1,4  3,2,1,5,9,8,7,4  3,2,1,4,9,8,7,5//3,2,1,4,9,8,7,5  1,2,3,4,9,8,7,5  1,2,3,4,5,8,7,9  1,2,3,4,5,7,8,9quick(array, 0, array.length-1);}//递归public static void quick(int[] array,int low,int high){if (low >= high) {return;}int p = partition(array, low, high); //表示  基准点元素所在的正确索引//确定左边分区范围quick(array,low,p-1);//确定右边分区范围quick(array, p+1, high);}//分区private static int partition(int[] array,int low,int high){ //low 表示左边界  high: 表示右边界int pv = array[high]; //选取最右边的元素为基准点元素int i = low;for (int j = low; j < high; j++) {if (array[j] < pv) {//下标小于基准点的元素swap(array, i, j); //将小于基准点的元素换到 下标为i 所在的位置i++;}}swap(array, high, i); //将基准点和 下标为i 的元素互换位置System.out.println(Arrays.toString(array)+"i="+i);//返回值表示基准点元素所在的正确索引,以此来确定下一轮的边界分区return i;}public static void swap(int[] array,int i,int j){//将数组中下标为i和j的元素分别交换位置int t = array[i]; //先将下标为 i 的元素取出来放进缓存区array[i] = array[j];//将下标为 j 的元素放到原先下标为 i 的位置array[j] = t; //将下标为 i 的元素 从缓存区里取出来放入 原先下标为 j 的位置}
}

运行结果:

[3, 2, 1, 4, 9, 8, 7, 5]i=3
[1, 2, 3, 4, 9, 8, 7, 5]i=0
[1, 2, 3, 4, 9, 8, 7, 5]i=2
[1, 2, 3, 4, 5, 8, 7, 9]i=4
[1, 2, 3, 4, 5, 8, 7, 9]i=7
[1, 2, 3, 4, 5, 7, 8, 9]i=5

3.双边循环快排

基本步骤: 

  1. 选择最左元素作为基准点元素

  2. j 指针负责从右向左找比基准点小的元素,i 指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至 i,j 相交

  3. 最后基准点与 i(此时 i 与 j 相等)交换,i 即为分区位置

代码实现:

public class QuickSort2 {public static void main(String[] args) {int[] array = {5,3,7,2,9,8,1,4};// 5,3,4,2,9,8,1,7  5,3,4,2,1,8,9,7  1,3,4,2,5,8,9,7// 1,3,2,4,5,8,9,7  1,2,3,4,5,8,9,7  1,2,3,4,5,8,7,9  1,2,3,4,5,7,8,9quick(array, 0, array.length-1);}//递归public static void quick(int[] array,int low,int high){if (low >= high) {return;}int p = partition(array, low, high); //表示  基准点元素所在的正确索引//确定左边分区范围quick(array,low,p-1);//确定右边分区范围quick(array, p+1, high);}//分区private static int partition(int[] array,int low,int high){ //low 表示左边界  high: 表示右边界int pv = array[low]; //选取最左边的元素为基准点元素int i = low; //i 开始位于左边界int j = high; // j 开位于右边界while (i < j){//j 从最右边的元素开始找 小于基准点的元素while (i < j && array[j] > pv) { // i < j -- 防止 i 和 j发生越界j--;}//i 从最左边的元素开始找 大于基准点的元素while (i < j && array[i] <= pv) { //array[i] <= pv -- i开始位于左边界,且 pv 为左边界的元素 如果不相等,就无法进入循环i++;}swap(array, i, j); // j 找到小于基准点的元素  i找到大于基准点的元素,两者位置发生互换}swap(array, low, j); //基准点 和 i(i和j相等) 互换,i 为 新的分区位置System.out.println(Arrays.toString(array) + "j=" + j);return j;}public static void swap(int[] array,int i,int j){//将数组中下标为i和j的元素分别交换位置int t = array[i]; //先将下标为 i 的元素取出来放进缓存区array[i] = array[j];//将下标为 j 的元素放到原先下标为 i 的位置array[j] = t; //将下标为 i 的元素 从缓存区里取出来放入 原先下标为 j 的位置}
}

运行结果:

[1, 3, 4, 2, 5, 8, 9, 7]j=4
[1, 3, 4, 2, 5, 8, 9, 7]j=0
[1, 2, 3, 4, 5, 8, 9, 7]j=2
[1, 2, 3, 4, 5, 7, 8, 9]j=6

4.快速排序的特点 

  1. 平均时间复杂度是 O(nlog₂⁡n ),最坏时间复杂度 O(n²)

  2. 数据量较大时,优势非常明显

  3. 属于不稳定排序

5.洛穆托分区方案 vs 霍尔分区方案

        霍尔的移动次数平均来讲比洛穆托少3倍

十、ArrayList--扩容规则

扩容规则:

  1. ArrayList() 会使用长度为零的数组 (调用无参构造的时候)

  2. ArrayList(int initialCapacity) 会使用指定容量的数组(调用有参构造的时候,其中参数为整形)

  3. public ArrayList(Collection<? extends E> c) 会使用 c 的大小作为数组容量(调用有参构造的时候,其中参数为集合)

  4. add(Object o) 首次扩容为 10,再次扩容为上次容量的 1.5 倍(这里用的是位运算)

  5. addAll(Collection c) 没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)

十一、lterator_FailFast 和 lterator_FailSafe

1.基本介绍

  • fail-fast : 一旦发现遍历的同时,其他人来修改,则立刻抛出异常
  • fail-safe : 发现遍历的同时,其他人来修改,应当有应对策略,例如牺牲一致性来让整个遍历运行完成

2.两者区别(代码演示)

@SuppressWarnings("all")
public class FailFastVsFailSafe {private static void failFast(){ArrayList<Student> list = new ArrayList<>();list.add(new Student("A"));list.add(new Student("B"));list.add(new Student("C"));list.add(new Student("D"));for (Student student : list) {System.out.println(student);}System.out.println(list);}private static void failSafe(){CopyOnWriteArrayList<Student> list = new CopyOnWriteArrayList<>();list.add(new Student("A"));list.add(new Student("B"));list.add(new Student("C"));list.add(new Student("D"));for (Student student : list) {System.out.println(student);}System.out.println(list);}static class Student{String name;public Student(String name) {this.name = name;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +'}';}}public static void main(String[] args) {failFast();}
}

接下来分别演示两者的区别:

在 failFast()方法中的 for循环遍历list 处加上断点,并在断点上添加条件  student.name.equals("C") ,用debug来调试程序,启动程序后,在调试器页面可以看到 ArrayList 集合中有四个元素,此时模拟另外一个线程来向 ArrayList 中添加一个元素(Alt + F8)

添加之后继续运行程序:

在 failSafe()方法中重复上述步骤,查看运行结果:

可以发现它的读写是分离开来的

典型代表:

  • ArrayList 、vector 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败

  • CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离

十二、ArrayList 和 LinkedList

1.两者对比

ArrayList

  1. 基于数组,需要连续内存

  2. 随机访问快(指根据下标访问)

  3. 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低

  4. 可以利用 cpu 缓存,局部性原理

LinkedList

  1. 基于双向链表,无需连续内存

  2. 随机访问慢(要沿着链表遍历)

  3. 头尾插入删除性能高

  4. 占用内存多

十三、HashMap

1.基本数据结构

【面试题】底层数据结构,jdk1.7 与 jdk1.8 有何不同? 

  • 在 jdk1.7 中,HashMap的基本数据结构为 数组+链表
  • 在 jdk1.8 中,HashMap的基本数据结构为 数据+(链表|红黑树)

2.树化与退化

【面试题】为何要用红黑树,为何一上来不树化,树化的阈值为何是8,何时会树化,何时会退化成链表?

树化意义:

  • 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略

  • hash 表的查找,更新的时间复杂度是 O(1),而红黑树的查找,更新的时间复杂度是 O(log₂N),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表

  • hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小

树化规则

  • 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化

退化规则

  • 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表

  • 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表(看的是移除之前)

3.索引计算

【面试题】索引如何计算?hashCode 都有了,为何还要提供 hash()方法?数组容量为何是2的n次幂?

索引计算方法:

  • 首先,计算对象的 hashCode()

  • 再进行调用 HashMap 的 hash() 方法进行二次哈希

    • 二次 hash() 是为了综合高位数据,让哈希分布更为均匀

  • 最后 & (capacity – 1) 得到索引  (capacity:容量)

数组容量为何是 2 的 n 次幂

  1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模

  2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap (oldCap:原始的容量)

注意

  • 二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash

  • 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable

4.put与扩容

【面试题】介绍一下put方法流程 ,jdk1.7 与 jdk1.8  有何不同?

put 流程:

  1. HashMap 是懒惰创建数组的,首次使用才创建数组

  2. 计算索引(桶下标)

  3. 如果桶下标还没人占用,创建 Node 占位返回

  4. 如果桶下标已经有人占用

    1. 已经是 TreeNode 走红黑树的添加或更新逻辑

    2. 是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑

  5. 返回前检查容量是否超过阈值,一旦超过进行扩容

put在 jdk1.7 与 1.8 的区别:

  1. 链表插入节点时,1.7 是头插法,1.8 是尾插法

  2. 1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容

  3. 1.8 在扩容计算 Node 索引时,会优化

扩容(加载)因子为何默认是 0.75f:

  1. 在空间占用与查询时间之间取得较好的权衡

  2. 大于这个值,空间节省了,但链表就会比较长影响性能

  3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多

5.并发问题

 1.扩容死链(jdk1.7存在)

        jdk1.7源码:

void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;for (Entry<K,V> e : table) {while(null != e) {Entry<K,V> next = e.next;if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;}}
}
  • e 和 next 都是局部变量,用来指向当前节点和下一个节点

  • 线程1(绿色)的临时变量 e 和 next 刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程2(蓝色)完成扩容和迁移

  • 线程2 扩容完成,由于头插法,链表顺序颠倒。但线程1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移

  • 第一次循环

    • 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b

    • e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)

    • 当循环结束是 e 会指向 next 也就是 b 节点

  • 第二次循环

    • next 指向了节点 a

    • e 头插节点 b

    • 当循环结束时,e 指向 next 也就是节点 a

  • 第三次循环

    • next 指向了 null

    • e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成

    • 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出

2.数据错乱(jdk1.7 和 jdk1.8都存在) 

6.key的设计

key 的设计要求

  1. HashMap 的 key 可以为 null,但 Map 的其他实现则不然

  2. 作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变)

  3. key 的 hashCode 应该有良好的散列性

String 对象的 hashCode() 设计

  • 目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特

  • 字符串中的每个字符都可以表现为一个数字,称为 Sᵢ,其中 i 的范围是 0 ~ n - 1

  • 散列公式为: S∗31ⁿ⁻¹+ S₁∗31ⁿ⁻²+ … Sᵢ∗ 31ⁿ⁻¹⁻ᶦ+ …Sₙ₋₁∗31⁰

  • 31 代入公式有较好的散列特性,并且 31 * h 可以被优化为

    • 即 32 ∗h -h

    • 即 2⁵ ∗h -h

    • 即 h≪5 -h

十四、单例模式

1.单例模式之饿汉式

代码实现:

public class Singleton1 implements Serializable {private Singleton1(){ //构造方法设置成私有的,避免其他类调用构造来创建实例对象if (INSTANCE != null) {throw new RuntimeException("单例对象不能被重复创建"); //防止反射破坏单例}System.out.println("private Singleton1");}//给静态变量赋值会放入静态代码块中执行,JVM保证线程安全private static final Singleton1 INSTANCE = new Singleton1(); //调用私有构造创建唯一的实例public static Singleton1 getInstance(){return INSTANCE;}public static void otherMethod(){System.out.println("otherMethod()");}public Object readResolve(){return INSTANCE;}
}

如果在构造方法中不抛出异常,就可以通过反射破坏单例,测试代码:

public class TestSingleton {public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {Singleton1.otherMethod();System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>");System.out.println(Singleton1.getInstance());System.out.println(Singleton1.getInstance());//反射破坏单例reflection(Singleton1.class);//反序列化破坏单例//serializable(Singleton1.getInstance());//unSafe 破坏单例//unsafe(Singleton1.class);}private static void reflection(Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {Constructor<?> constructor = clazz.getDeclaredConstructor();constructor.setAccessible(true);System.out.println("反射创建实例:" + constructor.newInstance());}
}

 运行结果:

private Singleton1
otherMethod()
>>>>>>>>>>>>>>>>>>>>>>>>>>>
day01.pattern.Singleton1@27d6c5e0
day01.pattern.Singleton1@27d6c5e0
private Singleton1
反射创建实例:day01.pattern.Singleton1@5f184fc6

可以看到,通过反射这种方式重新调用 Singleton1  的构造方法,并且创建出另外一个实例,此时已经破坏了单例

如果不加 readResolve() 方法,就可以通过反序列化破坏单例,测试代码:

 private static void serializable(Object instance) throws IOException, ClassNotFoundException {ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);oos.writeObject(instance);ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));System.out.println("反序列化创建单例:" + ois.readObject());}

运行结果:

private Singleton1
otherMethod()
>>>>>>>>>>>>>>>>>>>>>>>>>>>
day01.pattern.Singleton1@27d6c5e0
day01.pattern.Singleton1@27d6c5e0
反序列化创建单例:day01.pattern.Singleton1@64a294a6

同样破坏了单例

总结: 

  • 构造方法抛出异常是防止反射破坏单例

  • readResolve() 是防止反序列化破坏单例

2. 单例模式之枚举饿汉式

代码实现:

public enum Singleton2 {INSTANCE;Singleton2(){System.out.println("private Singleton2()");}@Overridepublic String toString(){return getClass().getName() + "@" + Integer.toHexString(hashCode());}public static Singleton2 getInstance(){return INSTANCE;}public static void otherMethod(){  //测试单例是饿汉式还是懒汉式System.out.println("otherMethod()");}}

修改上述的测试代码,测试反射这种方式是否可以破坏单例:

private static void reflection(Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {//Constructor<?> constructor = clazz.getDeclaredConstructor();Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);constructor.setAccessible(true);//System.out.println("反射创建实例:" + constructor.newInstance());System.out.println("反射创建实例:" + constructor.newInstance("OTHER",1));}

运行结果:

private Singleton2()
otherMethod()
>>>>>>>>>>>>>>>>>>>>>>>>>>>
day01.pattern.Singleton2@27d6c5e0
day01.pattern.Singleton2@27d6c5e0
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objectsat java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)at day01.pattern.TestSingleton.reflection(TestSingleton.java:44)at day01.pattern.TestSingleton.main(TestSingleton.java:18)

接下来测试反序列化是否可以破坏单例,测试结果:

private Singleton2()
otherMethod()
>>>>>>>>>>>>>>>>>>>>>>>>>>>
day01.pattern.Singleton2@27d6c5e0
day01.pattern.Singleton2@27d6c5e0
反序列化创建单例:day01.pattern.Singleton2@27d6c5e0

3.单例模式之懒汉式

 代码实现:

public class Singleton3 implements Serializable {private Singleton3(){System.out.println("private Singleton3()");}private static Singleton3 INSTANCE = null;public static synchronized Singleton3 getInstance(){if (INSTANCE == null) {INSTANCE = new Singleton3();}return INSTANCE;}public static void otherMethod(){System.out.println("otherMethod()");}
}

其实只有首次创建单例对象才需要同步,但该代码实际上每次调用都会同步,这样就导致性能较为低

4.单例模式之双检索懒汉式

代码实现: 

public class Singleton4 implements Serializable {private Singleton4(){System.out.println("private Singleton4()");}private static volatile Singleton4 INSTANCE = null; // volatile : 解决了共享变量的 可见性和有序性public static Singleton4 getInstance(){if (INSTANCE == null) {synchronized (Singleton4.class) {if (INSTANCE == null) {INSTANCE = new Singleton4();}}}return INSTANCE;}public static void otherMethod(){System.out.println("otherMethod()");}
}

加 volatile 的作用:

  • INSTANCE = new Singleton4() 不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值,其中后两步可能被指令重排序优化,变成先赋值、再调用构造

  • 如果线程1 先执行了赋值,线程2 执行到第一个 INSTANCE == null 时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象

5.单例模式之内部类

代码实现: 

public class Singleton5 implements Serializable {private Singleton5(){System.out.println("private Singleton5");}private static class Holder{static Singleton5 INSTANCE = new Singleton5();}public static Singleton5 getInstance(){return Holder.INSTANCE;}public static void otherMethod(){System.out.println("otherMethod()");}
}

6.JDK中单例的实现

  • Runtime 体现了饿汉式单例

  • Console 体现了双检锁懒汉式单例

  • Collections 中的 EmptyNavigableSet 内部类懒汉式单例

  • ReverseComparator.REVERSE_ORDER 内部类懒汉式单例

  • Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例

JAVA面试八股文宝典(黑马学习随笔)-- 基础篇相关推荐

  1. 计算机视觉面试宝典--深度学习机器学习基础篇(四)

    计算机视觉面试宝典–深度学习机器学习基础篇(四) 本篇主要包含SVM支持向量机.K-Means均值以及机器学习相关常考内容等相关面试经验. SVM-支持向量机 支持向量机(support vector ...

  2. C++面试八股文快问快答の基础篇

    文章目录 基础篇 变量的声明和定义有什么区别 简述#ifdef.#else.#endif和#ifndef的作用 写出int .bool. float .指针变量与 "零值"比较的i ...

  3. 【Java面试八股文宝典之基础篇】备战2023 查缺补漏 你越早准备 越早成功!!!——Day10

    大家好,我是陶然同学,软件工程大三明年实习.认识我的朋友们知道,我是科班出身,学的还行,但是对面试掌握不够,所以我将用这100多天更新Java面试题

  4. 黑马 Java八股文面试题视频教程,Java面试八股文宝典(虚拟机篇)

    1. JVM 内存结构 要求 掌握 JVM 内存结构划分 尤其要知道方法区.永久代.元空间的关系 结合一段 java 代码的执行理解内存划分 执行 javac 命令编译源代码为字节码 执行 java ...

  5. 十一郎专栏 | java面试八股文-基础篇

    最近发现全网都在找八股文,看来最近面试的人真的很多. 铁汁们对于面试八股文的需求很大,但是资料是这里凑一点那里凑一点,不够系统化. 我根据自己的面试经验,以及拉了几个大佬+面试官讨论了一下,基础篇就出 ...

  6. 马士兵java面试八股文及答案

    马士兵java面试八股文及答案 Java面向对象有哪些特征,如何应用 HashMap原理是什么,在jdk1.7和1.8中有什么区别 ArrayList和LinkedList有什么区别 高并发中的集合有 ...

  7. 100+家公司的JAVA面试八股文,终于整理完了

    前言 又是一年面试跳槽季,你准备好了吗? 今天为大家整理了目前互联网出现率最高的大厂面试题,所谓八股文也就是指文章的八个部分,文体有固定格式:由破题.承题.起讲.入题.起股.中股.后股.束股八部分组成 ...

  8. 相当炸裂!阿里架构师最新产出Java面试突击宝典,直接霸榜Github

    Java作为一门历史悠久的编程语言,仍然是企业级应用开发的首选.然而,随着新兴技术的涌现,Java的地位逐渐被冲淡,同时开发人员数量过剩也给Java行业带来了竞争压力.为了保持竞争力,Java行业需要 ...

  9. Github获星99k的Java面试八股文,最全秋招面试攻略

    Github获星99k的Java面试八股文,最全秋招面试攻略 2020年我凭借一份<Java面试核心知识点>成功拿下了阿里.字节.小米等大厂的offer,两年的时间,为了完成我给自己立的f ...

最新文章

  1. zookeeper安装和使用 windows环境(转)
  2. canvas绘制弯月
  3. Spring 使用注解方式进行事物管理
  4. SQL compute by 的使用
  5. 使用高级管理控制台获得对Windows Home Server的扩展访问
  6. php7 匿名继承类_PHP7中的匿名类使用方法_后端开发
  7. linux退出lftp命令,lftp命令使用
  8. 反射,System.Type类
  9. python建立数据库和基本表_python基础 — 链接 Mysql 创建 数据库和创表
  10. Sublime 格式化 JSON
  11. 【CCCC】L2-012 关于堆的判断 (25分),,手写堆,二叉树编号,向上调整
  12. mysql sqrt_详解MySQL中的SQRT函数的使用方法
  13. Cadence17.2版本原理图绘制
  14. H3C基础配置命令(一)
  15. iOS进阶:【1、 使用文件路径获取自定义字体名称2、添加资源包到工程→在info.plist文件中注册字体→在工程Bundle Resource中复制字体资源包→代码检测查询加入的字体并使用。】
  16. 单层感知器与线性神经网络
  17. python输入年份判断生肖_python根据出生年份简单计算生肖的方法
  18. 人间哪知星空遥:荣耀30系列的巡天计划
  19. 基于ZYNQ+AD9361的软件无线电平台设计与实现 (2)
  20. springboot服务器没响应,记一次springboot 故障:接口无响应--》CPU 100%---》log无法写入---》磁盘满了...

热门文章

  1. 【开发一个简单的音乐播放器+服务端】【一】
  2. Gstore官网学习一:知识图谱与gStore介绍
  3. 抖音滑块以及轨迹分析
  4. 2016-09-01[关于Paint笔触的了解]
  5. django qq邮箱发送验证码
  6. NSDI 2021 Breaking the Transience-Equilibrium Nexus: A New Approach to Datacenter Packet Transport笔记
  7. 颜色选择器---Kodu少儿编程第九天
  8. 基于觅食生境选择的改进粒子群算法-附代码
  9. vi模式下的编辑、删除、保存和退出
  10. 事业单位怎么发送通知短信