Loading

Java面试专题课 · 基础篇

BV15b4y117RJ

二分查找

目标:手写代码、掌握细节
细节:
1. 避免整数溢出:L+R可能超出Integer.MAX_VALUE。
方法一:改成 L/2+R/2 → L + (R-L)/2
方法二:改成位计算(无符号右移) (L+R) >>> 1
2. 变体 (详见leetcode)

排序

目标:掌握思路,手写代码,了解特性(时间复杂度、是否稳定)

冒泡排序

(升序)每轮 依次比较相邻两个元素的大小,若a[j]>a[j+1]则交换位置,使得最大的元素排到最后
优化:
1. 减少比较次数(每轮冒泡的比较次数递减)
2. 减少冒泡次数(发现本轮没有进行交换,说明数组整个都有序了不用再排序了),
3. 进一步优化:记录每轮的最后一次进行交换的索引,来得到下轮的比较次数。若为0这说明全排完了(相当于优化2)

选择排序

(升序)每轮选择出数组后部分中最小的元素,移到数组前部分
i代表每轮选择最小元素要交换到的目标索引

选择排序vs冒泡排序

image
稳定:相同元素不会打乱位置

插入排序

数组开头保持为有序的。把后面的元素一个一个插入到开头的有序组中。
性质:稳定的
image
缺点:很大的元素位于前面的话,要移动很多次位置才能到达目标位置希尔排序
改进了插入排序。把整个数组按一定间隔分组,分别进行插入排序插入和选择—推导第n轮后的结果

快速排序

1.每一轮排序选择一个基准点(pivot)进行分区
1.让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区2.当分区完成时,基准点元素的位置就是其最终位置
2.在子分区内重复以上过程,直至子分区元素个数少于等于1,这体现的是分而治之的思想(divide-and-conquer)

快排的实现

  • 单边循环快排(Lomuto):

    • pivot:最右元素
    • j指针负责找到比pivot小的元素,一旦找到则与i进行交换;i指针维护小于pivot的边界,即每次交换的目标索引
    • 最后pivot和i交换,i即为分区位置
      image
  • 双边循环快排:

    • pivot:最左元素
    • j指针负责从右向左找比pivot小的元素,i指针负责从左向右找比基准点大的元素,一旦找到则两者交换,直到i、j相交
    • 最后pivot和i交换,i即为分区位置
    • 霍尔

快排的特点数据量特别大的时候用快排,其中分区长度比较小的时候用插排

集合—ArrayList

扩容机制

初始化:

  • ArrayList() 长度为0
  • ArrayList(int initialCapacity) 使用指定容量的数组
  • public ArrayList(Collection<? extends E> c)使用c的大小作为容量

添加元素超出容量时会扩容。

  • 添加一个元素 L.add 每次扩容成1.5倍
  • 添加一个集合的全部元素 L1.addAll(L2) ① L1是空list 扩容成max(10,实际元素个数) ② L1非空 扩容成max(L1的1.5倍,实际元素个数)Iterator

遍历

  • fail-fast 遍历的同时不能修改,尽快失败
    一旦发现遍历时其他人来修改,则抛异常
    ArrayList用modCount记录修改次数,遍历中检查发现modCount与预期不符则说明被修改过了,终止遍历

  • fail-safe 遍历的同时可以修改,原理是读写分离
    允许修改,牺牲一致性(以老数据为准)让遍历完成
    CopyOnWriteArrayListObject[] snapshot记录了快照用于遍历。添加和遍历的数组互不干扰

  • Vector是fail-fast的

LinkedList与ArrayList

image

  • ArrayList
  1. 基于数组,需要连续内存
  2. 随机访问快(根据下标访问,可以根据索引得到任意元素的地址)
  3. 尾部插入、删除性能好,其他部分插入、删除会移动数据,因此性能低
  4. 可以利用cpu缓存,局部性原理
  • LinkedList
  1. 基于双向链表,无需连续内存
  2. 随机访问慢(要沿着链表遍历)
  3. 头尾插入删除性能高
  4. 占用内存多

HashMap

Hash可以进行快速查找。

  • 1.7与1.8的不同?
    1.7 数组+链表, 1.8 数组+(链表|红黑树)树化阈值是8

  • 为什么要树化
    很多元素的桶下标相同,链表过长且扩容无法缩短链表长度。
    (正常情况下不会出现这种现象,一般是恶意行为如DoS攻击者刻意输入了很多Hash值相同的对象)
    形成红黑树可以减小检索次数,解决链表性能过低的问题
    hash表的查找、更新时间复杂度是O(1),红黑树的查找、更新时间复杂度是O(\(log_2(n)\)),TreeNode占用空间也比普通Node大。若非必要,尽量还是使用链表。

  • 树化的条件
    两个条件:链表长度>树化阈值8 ②数组容量>=64
    hash值如果足够随机,则在hash表内按泊松分布。选择8是为了让树化几率足够小

  • 树退化成链表的情况
    情况1:在扩容时如果拆分树时,树元素<=6则会退化成链表。
    情况2:remove树节点时root、root.left、root.right、root.left.left有一个为null

  • 索引的计算
    keys→hashCode→二次hash→取余数(&容量-1)得到索引
    二次hash()是为了综合高位数据,让哈希分布更均匀
    以上都是为了配合2的n次幂时的优化手段,例如Hashtable的容量不是2的n次幂,并不能说哪种设计更有,应该说是综合葛总因素选择了2的n次幂作为容量

  • 为什么需要二次hash
    让哈希分布更均匀,防止超长链表产生
    image

  • 容量为何是2的n次幂
    好处:数组容量是2的n次幂,可以使用位与运算代替取模,效率更高;扩容时hash&oldCap==0的元素留在原来位置,否则新位置=旧位置+oldCap
    问题:分布性不是很均匀。选择大质数更好(扩容时容量翻倍然后找下一个质数作为新容量)
    总结:2的n次幂→更高效率,大质数→更好的分布性

Put方法

  • HashMap是懒惰创建数组的,首次使用才创建数组
  • 计算索引(桶下标)
  • 如果桶下标还没被占用,创建Node占位返回
  • 如果桶下标被占用:
    • TreeNode 走红黑树的添加或更新逻辑
    • 普通Node 走链表的添加或更新逻辑;若链表长度超过树化阈值,走树化逻辑
  • 返回前检查容量是否超过阈值,超过则进行扩容逻辑 (添加完新元素才进行扩容)

版本不同:

  • 链表插入节点时,1.7是头插法,1.8是尾插法
    • 新加的元素在链表头部/尾部
  • 1.7在大于等于阈值且没有空位时扩容,1.8是大于阈值就扩容
  • 1.8在扩容计算Node索引时会优化

扩容因子

在用了75%的容量的时候就要进行扩容。
扩容因子为何默认是0.75f?

  • 在空间占用和查询时间之间取得较好的平衡
  • 大于这个值:节省空间,但链表比较长,影响性能
  • 小于这个值:减少冲突,但扩容更频繁,占用空间多

多线程下的HashMap

并发容易出现丢失数据的现象
问题:

  • 死链 (1.7) a→b→a→...
    头插法导致扩容死链
  • 数据错乱 (1.7, 1.8)

key相关

  • 能否为null?
    HashMap的可以为null。其他Map的实现不能
  • 作为key的对象有什么要求?
    必须实现hashCode和equals,并且key的内容不能修改(应该不可变。否则之后用此key找不到值了)

String对象的hashCode()

每个字符\(S_i\),字符串\(S\)\(S_0*31^{n-1}+S_1*31^{n-2}+\cdots+S_{n-1}*31^0\)

  • 为什么乘31?
    31代入公式具有较好的散列特性,且31*h可简化为\(32*h-h = h<<5 - h\) 方便位运算

设计模式

单例模式

一个类只有一个实例

目标:

  • 五种实现方式
  • JDK中哪些地方体现了单例模式

实现方式

1. 饿汉式

  • 构造私有
  • 静态成员变量
  • 公共的静态方法

三种破坏手段及预防方式

  • 反射破坏单例
    reflection(Singleton1.class);通过反射创建了一个对象。一个类有两个对象,不再是单例了
    image

  • 反序列化破坏单例
    实现Serializable时,serializable(Singleton1.class);反序列化造出了一个新对象。
    image

  • Unsafe破坏单例
    unsafe(Singleton1.class);目前没有预防方法

public class Singleton1 implements Serializable {
	private Singelton1() {
		// 防止反射破坏单例
		if(INSTANCE !=null) {
			throw new RuntimeException("单例对象不能重复创建");
		}
		System.out.println("private Singleton1()");
	}
	private static final Singleton1 INSTANCE = new Singleton1();	//静态成员变量
	public static Singleton1 getInstance() {
		return INSTANCE;
	}
	public static void otherMethos() {
	}
	// 防止反序列化破坏单例
	public Object readResolve() {
		return INSTANCE;
	}
}

2.枚举饿汉式

  • 不怕反射、反序列化破坏单例
  • Unsafe可以破坏单例
// 枚举类
enum Sex {
	MALE, FEMALE;
}

// 编译器最后还是会把enum编译成class。相当于
final class Sex extends Enum<Sex> {
	public static final Sex MALE;
	public static final Sex FEMALE;

	private Sex(String name, int ordinal) {
		super(name, ordinal);
	}

	static {
		MALE = new Sex("MALE", 0);
		FEMALE = new Sex("FEMALE", 1);
		$VALUES = values();
	}

	private static final Sex[] $VALUES;

	private static Sex[] $values() {
		return new Sex[]{MALE, FEMALE};
	}

	public static Sex[] values() {
		return $VALUES.clone();
	}

	public static Sex valueOf(String value) {
		return Enum.valueof(Sex.class, value);
	}
}

// 枚举饿汉式
public enum Singleton2 {
	INSTANCE;

	Singleton2() {
		System.out.println("private Singleton2()");
	}

}

3. 懒汉式

懒惰式创建:构造第一次时才会创建实例
需要考虑线程安全问题 解决:加上一个synchoronized

public class Singleton3 implements Serializable {
	private Singleton3() { }
	private static Singleton3 INSTANCE = null;
	public static synchoronized Singleton3 getInstance() {
		if(INSTANCE == null) {
			INSTANCE = new Singleton3();
		}
		return INSTANCE;
	}

}

4. DCL懒汉式

懒汉式的优化
Double Check Logic双检索:在加锁之前先判断
要加上volatile保证共享变量的可见性、有序性
new Singleton4 → Singleton4() → INSTANCE=对象

public class Singleton4 implements Serializable {
	private Singleton4() { }
	private static volatile Singleton4 INSTANCE = null;
	public static synchoronized Singleton4 getInstance() {
		if(INSTANCE == null) {
			synchronized (Singleton4.class) {
				if(INSTANCE == null) {
					INSTANCE = new Singleton4();
				}
			}
		}
		return INSTANCE;
	}

}

5. 内部类懒汉式

静态变量赋值一定是放在静态代码块中的,JVM保证了其线程安全性。
内部类既有懒汉式的特性,又保证了创建过程中的线程安全。

public class Singleton5 implements Serializable {
	private Singleton5() {}
	private static class Holder {
		static Singleton5 INSTANCE = new Singleton5();
	}
	public static Singleton5 getInstance() {
		return Holder.INSTANCE;
	}
}

JDK中的体现

  • Runtime类:饿汉式
    (System.exit 和 System.gc用到了Runtime)

  • System类中的Console:双检索懒汉式单例
    image

  • Collections类中:
    内部类懒汉式
    image

EMPTY_SET:饿汉
image

posted @ 2021-10-14 19:45  ljs9  阅读(66)  评论(0)    收藏  举报