Java集合数据结构

本文详细讲解了HashMap的实现原理,包括其数据结构、哈希算法、解决哈希冲突的方法、源码解析以及与同类数据结构的对比,如ArrayMap、HashTable、TreeMap等。同时,探讨了HashMap与HashSet的区别,以及在不同场景下的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、HashMap的实现原理、HashMap的数据结构、HashMap的源码理解、从HashMap源码角度说说怎么put数据、HashMap怎么解决hash冲突的。

extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

// 初始容量 16
// 最大容量 2^30
// 加载因子 0.75
// 扩容倍数 2
// 临界值 = 容量 * 加载因子 默认临界值为12
// 最大临界值 2^31-1
// 可以放key为null的kv对
// Node<K,V> -> 数组、链表、红黑树

static class Node<K,V> implements Map.Entry<K,V> {
	final int hash;
	final K key;
	V value;
	Node<K,V> next;

	Node(int hash, K key, V value, Node<K,V> next) {
		this.hash = hash;
		this.key = key;
		this.value = value;
		this.next = next;
	}
}

// 链表的长度达到8个,数组的长度达到64个 -> 链表会树化成红黑树
// 链表的长度达到6个 -> 红黑树反树化为链表

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
	TreeNode<K,V> parent;  // red-black tree links
	TreeNode<K,V> left;
	TreeNode<K,V> right;
	TreeNode<K,V> prev;    // needed to unlink next upon deletion
	boolean red;
	TreeNode(int hash, K key, V val, Node<K,V> next) {
		super(hash, key, val, next);
	}
}

// HashMap 的哈希算法(怎么减少哈希碰撞)
// key.hashCode() -> public native int hashCode(); -> 由第三方语言库(c/c++)实现
// h ^ (h >>> 16) -> h ^ (h/65536)扰动算法、减少哈希碰撞
// key.hashCode() 会产生一个大数(> 65536的数),如果不对 h 进行扰动运算会造成散列值(> 65536的数)聚集

static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// jdk7的并发扩容rehash死循环问题
// 头插法

void createEntry(int hash, K key, V value, int bucketIndex) {
	//1:如果table[bucketIndex]位置没有链表,则e指向null
	//2:如果table[bucketIndex]位置有链表,则在table[bucketIndex]头部加入一个元素,其e指向原来的table[bucketIndex]链表
	Entry<K,V> e = table[bucketIndex];
	table[bucketIndex] = new Entry<>(hash, key, value, e);
	size++;
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
	Node<K,V>[] tab; Node<K,V> p; int n, i;
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
	if ((p = tab[i = (n - 1) & hash]) == null)
		tab[i] = newNode(hash, key, value, null);
	else {
		Node<K,V> e; K k;
		// 判断key是否重复
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
		else if (p instanceof TreeNode)
			e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		else {
			for (int binCount = 0; ; ++binCount) {
				if ((e = p.next) == null) {
					p.next = newNode(hash, key, value, null);
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
						treeifyBin(tab, hash);
					break;
				}
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;
			}
		}
		if (e != null) { // existing mapping for key
			V oldValue = e.value;
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;
			afterNodeAccess(e);
			return oldValue;
		}
	}
	++modCount;
	if (++size > threshold)
		resize();
	afterNodeInsertion(evict);
	return null;
}

// jdk8的并发扩容会有数据覆盖问题

final Node<K,V>[] resize() {
	Node<K,V>[] oldTab = table;
	int oldCap = (oldTab == null) ? 0 : oldTab.length;
	int oldThr = threshold;
	int newCap, newThr = 0;
	if (oldCap > 0) {
		if (oldCap >= MAXIMUM_CAPACITY) {
			threshold = Integer.MAX_VALUE;
			return oldTab;
		}
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
				 oldCap >= DEFAULT_INITIAL_CAPACITY)
			newThr = oldThr << 1; // double threshold
	}
	else if (oldThr > 0) // initial capacity was placed in threshold
		newCap = oldThr;
	else {               // zero initial threshold signifies using defaults
		newCap = DEFAULT_INITIAL_CAPACITY;
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}
	if (newThr == 0) {
		float ft = (float)newCap * loadFactor;
		newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
				  (int)ft : Integer.MAX_VALUE);
	}
	threshold = newThr;
	@SuppressWarnings({"rawtypes","unchecked"})
		Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	table = newTab;

	// 尾插法:将原有链表分为高低位两条链表分别放在扩容后对应下标位置
	if (oldTab != null) {
	  //遍历原来的table,bucket
	  for (int j = 0; j < oldCap; ++j) {
		 HashMap.Node<K, V> e;//bucket
		 if ((e = oldTab[j]) != null) {//如果当前桶位上有元素,不为null
			 oldTab[j] = null;//当前位置置空
			 if (e.next == null)//如果只有一个元素e,e.next为null,直接安排e到新表新家
				 newTab[e.hash & (newCap - 1)] = e;
			 else if (e instanceof HashMap.TreeNode)//判断e是不是树形节点,也就是超过8个元素
				 ((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);//
			 else { // preserve order
				 HashMap.Node<K, V> loHead = null, loTail = null;//低位的头结点,尾结点
				 HashMap.Node<K, V> hiHead = null, hiTail = null;//高危的头结点,尾结点
				 HashMap.Node<K, V> next;//下个节点
				 do {//在这个循环里,依次处理该桶上链表,分裂成高位链和低位链
					 next = e.next;//后一个节点元素
					 //如果元素的hash值,与 原来表的容量 等于0,实际上是把原来在一个桶位的元素分流
					 //例如e.hash & 10000,值会有0和1两种,等于0的,还是相对于原表的索引位置
					 //等于1,把他向高位调整
					 if ((e.hash & oldCap) == 0) {
						 if (loTail == null)//只有第一次进来的时候,loTail为null
							 loHead = e;//设置头结点位e
						 else
							 loTail.next = e;//尾插
						 loTail = e;//低位链指针下移
					 } else {
						 if (hiTail == null)
							 hiHead = e;
						 else
							 hiTail.next = e;
						 hiTail = e;
					 }
				 } while ((e = next) != null);
				 //while循环完之后,大概会形成两个链表【高位链hiHead--hiTail,低位链loHead--loTail】,最极端的情况是只有高位链或
				 //只有低位链。拿着这两个链表,插入到对应桶位,入驻新家。
				 //判断低位链中是否有元素
				 if (loTail != null) {
					 loTail.next = null;//干掉低位链尾部的next,因为e的下一个结点很可能被分到高位,所以我们要干掉这个叛徒
					 newTab[j] = loHead;
				 }
				 //判断高位链中是否有元素
				 if (hiTail != null) {
					 hiTail.next = null;
					 newTab[j + oldCap] = hiHead;
				 }
			 }
		 }
	  }
	}
	return newTab;
}

红黑树的插入定义:红黑树是一种有红黑节点的自平衡二叉查找树
1)每个节点要么是红色,要么是黑色
2)根节点是黑色
3)每个叶子节点(虚节点:NIL)是黑色
4)每个红色节点的两个子节点一定是黑色
5)任意一节点到每个叶子节点的路径都含有数量相同的黑节点

红黑树的插入
put 一个红色节点,根据二分查找法找到其位置:
1)c节点为根节点:将c染为黑色
2)c节点的p节点为黑色:什么也不错
3)c节点的p和u节点为红色:将gpu变色,如g变红则以g为c递归1234情况
4)c节点的p红色、u节点为黑色或为NIL:
(1):cpg三点为直线:以p为圆心g为旋转节点自旋,将p和g节点变色
(2):cpg三点为三角:以c为圆心p为旋转节点自旋,重复4-1情况

红黑树的删除
far nephew:远侄子
near nephew: 近侄子
brother:兄弟
left son:左儿子节点
right son:右儿子节点
left max:左子树最大节点

1)c节点为红色:直接删除
2)c节点为黑色,且无子节点:删除c
(1)b存在为黑色,p为黑色,fn存在为红色:以b为圆心p为旋转节点自旋,fn变为黑色。
(2)b存在为黑色,p为黑色,fn不存在,nn存在为黑色:以nn为圆心b为旋转节点自旋,将nn变红和b变黑,以c(黑色NIL节点)执行2-1情况。
(3)b存在为黑色,p为黑色,b无子节点(fn和nn不存在):将b染为红色,将p作为c重新执行2-1情况。
(4)b存在为黑色,p为红色:将b变为红色,p变为黑色。
3)c节点为黑色,有一个红色子节点(ls或rs):删除c,子节点(ls或rs)变为黑色,将p的左指针指向子节点(ls或rs)。
4)c节点为黑色,有两个s节点:找到左子树的最大节点lx,将lx赋值给c,将lx变为c递归执行以上所有。

2、HashMap怎么手写实现
think:
1)数据结构
2)哈希值和哈希冲突
3)初始值和扩容
4)put()/get()/remove()

3、集合Set实现Hash怎么防止碰撞
Set是一个接口,最常用的实现类就是HashSet,HashSet -> HashMap ,说下HashMap 是怎么防止碰撞的。

if (p.hash == hash &&
  ((k = p.key) == key || (key != null && key.equals(k))))

3、ArrayMap和HashMap的对比

1)从数据结构上:
ArrayMap是两个数组:一个存key的hashCode,一个存key-value对。
HashMap是数组+链表+红黑树:Node[](Entry[])、Node、TreeNode

2)从解决哈希碰撞的方案上:
ArrayMap 使用的是开放寻址法
HashMap 使用的是扰动算法加链表结构

3)从内存开销上
ArrayMap 使用的内存开销小
HashMap 使用的内存开销大

4)从数据量级来说
ArrayMap 处理数据量不大,最好在千级以内
HashMap 处理的数据量级远远高于ArrayMap

5)从线程安全来说:都是线程不安全的

6)细节:都可以存放key为null的k-v对

// ArrayMap 数据结构和哈希冲突
// ArrayMap是Android特有的api,用在移动端,所以它主要是提高内存效率

public final class ArrayMap<K, V> implements Map<K, V> {
}

int[] mHashes; // 存储出的是每个key的hash值,并且在这些key的hash值在数组当中是从小到大排序的。
Object[] mArray; // 长度是mHashs的两倍,每两个元素分别是key和value,这两元素对应mHashs中的hash值。

// 如果key存在,则返回oldValue

public V put(K key, V value) {
	//key的hash值
	final int hash;
	//下标
	int index;
	// 如果key为null,则hash值为0.
	if (key == null) {
		hash = 0;
		//寻找null的下标
		index = indexOfNull();
	} else {
		//根据mIdentityHashCode 取到 hash值
		hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
		//根据hash值和key 找到合适的index
		index = indexOf(key, hash);
	}
	//如果index>=0,说明是替换(改)操作
	if (index >= 0) {
		//只需要更新value 不需要更新key。因为key已经存在
		index = (index<<1) + 1;
		//返回旧值
		final V old = (V)mArray[index];
		mArray[index] = value;
		return old;
	}
	//index<0,说明是插入操作。 对其取反,得到应该插入的下标
	index = ~index;  // ~x=-(x+1);
	//如果需要扩容
	if (mSize >= mHashes.length) {
		//如果容量大于8,则扩容一半。
		//否则容量大于4,则扩容到8.
		//否则扩容到4
		final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
				: (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
		//临时数组
		final int[] ohashes = mHashes;
		final Object[] oarray = mArray;
		//分配空间完成扩容
		allocArrays(n);
		//复制临时数组中的数组进新数组
		if (mHashes.length > 0) {
			if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0");
			System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
			System.arraycopy(oarray, 0, mArray, 0, oarray.length);
		}
		//释放临时数组空间
		freeArrays(ohashes, oarray, mSize);
	}
	//如果index在中间,则需要移动数组,腾出中间的位置
	if (index < mSize) {
		if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index)
				+ " to " + (index+1));
		System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index);
		System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
	}
	//hash数组,就按照下标存哈希值
	mHashes[index] = hash;
	//array数组,根据下标,乘以2存key,乘以2+1 存value
	mArray[index<<1] = key;
	mArray[(index<<1)+1] = value;
	mSize++;//修改size
	return null;
}

// 根据key和key的hash值,返回index

int indexOf(Object key, int hash) {
	//N为当前集合size 
	final int N = mSize;
	//如果当前集合是空的,返回~0
	if (N == 0) {
		return ~0;
	}
	//根据hash值,通过二分查找,查找到目标index
	int index = ContainerHelpers.binarySearch(mHashes, N, hash);
	//如果index < 0,说明该hash值之前没有存储过数据
	if (index < 0) {
		return index;
	}
	//如果index>=0,说明该hash值,之前存储过数据,找到对应的key,比对key是否相等。相等的话,返回index。说明要替换。
	if (key.equals(mArray[index<<1])) {
		return index;
	}

	
	// 由于二分法查找只是找到对应的hashcode的值,但是如果存在多个相同的hashcode,但是key不一样的情况下,
	// 二分法只能定位到里面随机的一个,但是不可能知道这一个hashcode位于这些相同的hashcode值中的第几个
	
	//从index+1,遍历到数组末尾,找到hash值相等,且key相等的位置,返回
	int end;
	for (end = index + 1; end < N && mHashes[end] == hash; end++) {
		if (key.equals(mArray[end << 1])) return end;
	}

	// 如果找到最后一个hashcode一致但是key仍不一样,那就反过来以index-1位尾,向前挨个的寻找
	//从index-1,遍历到数组头,找到hash值相等,且key相等的位置,返回
	for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
		if (key.equals(mArray[i << 1])) return i;
	}
	
	// 如果出现极端情况下,仍然没有找到对应的key的情况,那么是说明这个key真的不在arraymap当中,但是这个key对应的hashcode是存在于mHashes中的
	// 这时候就返回~end,其实就是返回着一些hashcode的序列的最后一个+1。这样有了endindex就可以把这个key插入到着一些hashcodes中的最后一位了
	return ~end;
}

4、Hashtable的实现原理、HashMap和HashTable的区别

1)数据结构:Entery数组 + 链表
2)哈希算法是才用取模运算,没有使用扰动算法
3)扩容时采用的是头插法
4)synchronized标记,是线程安全的
5)不支持放key为null的kv对

public class Hashtable<K,V> extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable

// 初始容量 11
// 最大容量 2^31 - 9
// 加载因子 0.75
// 临界值 = 容量 * 加载因子 默认临界值为8
// 最大临界值 2^31
// 扩容倍数 2n + 1;

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Entry<K,V> next;
}
	
	
public synchronized V put(K key, V value) {
	// Make sure the value is not null
	if (value == null) {
		throw new NullPointerException();
	}

	// Makes sure the key is not already in the hashtable.
	Entry<?,?> tab[] = table;
	int hash = key.hashCode();
	
	// hash & 0x7FFFFFFF -> hash <= 0x7FFFFFFF ? hash : 0x7FFFFFFF;
	int index = (hash & 0x7FFFFFFF) % tab.length;
	@SuppressWarnings("unchecked")
	Entry<K,V> entry = (Entry<K,V>)tab[index];
	for(; entry != null ; entry = entry.next) {
		if ((entry.hash == hash) && entry.key.equals(key)) {
			V old = entry.value;
			entry.value = value;
			return old;
		}
	}

	addEntry(hash, key, value, index);
	return null;
}


private void addEntry(int hash, K key, V value, int index) {
	// think:为什么覆盖put的时候不会去修改modCount??? 
	// HashMap也是
	modCount++;


	Entry<?,?> tab[] = table;
	if (count >= threshold) {
		// Rehash the table if the threshold is exceeded
		rehash();

		// 扩容后要重新计算hashCode
		tab = table;
		hash = key.hashCode();
		index = (hash & 0x7FFFFFFF) % tab.length;
	}

	
	// 头插法:把新的Entry放在index位置并设置指针为旧的tab[index] 
	Entry<K,V> e = (Entry<K,V>) tab[index];
	tab[index] = new Entry<>(hash, key, value, e);
	count++;
}

protected void rehash() {
	int oldCapacity = table.length;
	Entry<?,?>[] oldMap = table;

	// overflow-conscious code
	// 扩容倍数  2n + 1;
	int newCapacity = (oldCapacity << 1) + 1;
	if (newCapacity - MAX_ARRAY_SIZE > 0) {
		if (oldCapacity == MAX_ARRAY_SIZE)
			// Keep running with MAX_ARRAY_SIZE buckets
			return;
		newCapacity = MAX_ARRAY_SIZE;
	}
	Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

	modCount++;
	threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
	table = newMap;

	for (int i = oldCapacity ; i-- > 0 ;) {
		for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
			Entry<K,V> e = old;
			old = old.next;

			int index = (e.hash & 0x7FFFFFFF) % newCapacity;
			e.next = (Entry<K,V>)newMap[index];
			newMap[index] = e;
		}
	}
}

5、 TreeMap的具体实现
1)数据结构:红黑树
2)没有哈希冲突
3)不支持存放key为null的kv对
4)红黑树性质、节点的插入和删除详见HashMap

public class TreeMap<K,V> extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable


static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
		
}

6、HashMap和HashSet的区别、HashSet和HashMap怎么判断元素集合重复

1)从数据结构上:HashSet是一个所有value为PRESENT的HashMap
2)判断集合重复的方法是 :判断key的hashcode和key值是否相等
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))

public class HashSet<E> extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
	
private transient HashMap<E,Object> map;

private static final Object PRESENT = new Object();

public HashSet() {
	map = new HashMap<>();
}

7、LinkedHashMap的结构

1)从数据结构上:在HashMap上加了两个指针before、after
2)支持插入顺序和访问顺序(LRU)
3)其它特性和HashMap一致

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

// false 基于插入顺序 true 基于访问顺序
// 此值默认是false。
// 基于访问的顺序get一个元素后,这个元素被加到最后(使用了LRU 最近最少被使用的调度算法)
final boolean accessOrder;

// LinkedHashMap的Entry比HashMap还多before, after
static class Entry<K,V> extends HashMap.Node<K,V> {
	Entry<K,V> before, after;
	Entry(int hash, K key, V value, Node<K,V> next) {
		super(hash, key, value, next);
	}
}


// 每次加入新节点时将 tail 与 p相连: tail.after = p  p.before = tail
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
	LinkedHashMap.Entry<K,V> last = tail;
	tail = p;
	if (last == null)
		head = p;
	else {
		p.before = last;
		last.after = p;
	}
}

void reinitialize() {
	super.reinitialize();
	head = tail = null;
}

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
	LinkedHashMap.Entry<K,V> p =
		new LinkedHashMap.Entry<K,V>(hash, key, value, e);
	linkNodeLast(p);
	return p;
}

Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
	LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
	LinkedHashMap.Entry<K,V> t =
		new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
	transferLinks(q, t);
	return t;
}

TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
	TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
	linkNodeLast(p);
	return p;
}

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
	LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
	TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
	transferLinks(q, t);
	return t;
}

void afterNodeRemoval(Node<K,V> e) { // unlink
	LinkedHashMap.Entry<K,V> p =
		(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
	p.before = p.after = null;
	if (b == null)
		head = a;
	else
		b.after = a;
	if (a == null)
		tail = b;
	else
		a.before = b;
}

void afterNodeInsertion(boolean evict) { // possibly remove eldest
	LinkedHashMap.Entry<K,V> first;
	if (evict && (first = head) != null && removeEldestEntry(first)) {
		K key = first.key;
		removeNode(hash(key), key, null, false, true);
	}
}

void afterNodeAccess(Node<K,V> e) { // move node to last
	LinkedHashMap.Entry<K,V> last;
	if (accessOrder && (last = tail) != e) {
		LinkedHashMap.Entry<K,V> p =
			(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
		p.after = null;
		if (b == null)
			head = a;
		else
			b.after = a;
		if (a != null)
			a.before = b;
		else
			last = b;
		if (last == null)
			head = p;
		else {
			p.before = last;
			last.after = p;
		}
		tail = p;
		++modCount;
	}
}

8、ArrayList 和 LinkedList 的区别以及应用场景、数组和链表的区别

10 —> 1.5n -> Arrays.copyof() -> Object[] -> Integer.MAX_VALUE - 8

线程不安全:
Vector -> Collections.synchronizedList() -> CopOnwriteArrayList

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

数据结构:双向链表
LinkedList还实现了Deque接口,Deque接口是Queue接口的子接口,代表一个双向队列

public class LinkedList<E> extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
		
private static class Node<E> {
	E item;
	Node<E> next;
	Node<E> prev;

	Node(Node<E> prev, E element, Node<E> next) {
		this.item = element;
		this.next = next;
		this.prev = prev;
	}
}

对比:
1)ArrayList使用数组,随机查询快,增删慢。
2)LinkedList使用双向链表,增删快,随机查询慢
3)LinkedList需要更多的内存

数组的优点
随机访问性强,查找速度快,时间复杂度为O(1)

数组的缺点
1.头插和头删的效率低,时间复杂度为O(N)
2.空间利用率不高
3.内存空间要求高,必须有足够的连续的内存空间
4.数组空间的大小固定,不能动态拓展

链表的优点
1.任意位置插入元素和删除元素的速度快,时间复杂度为O(1)
2.内存利用率高,不会浪费内存
3.链表的空间大小不固定,可以动态拓展

链表的缺点
随机访问效率低,时间复杂度为0(N)

9、CopyOnwriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap的实现原理

写时复制List:写入效率低,读取效率高。每次写入数据,都会创建一个新的底层数组

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
	
final transient ReentrantLock lock = new ReentrantLock();

final transient ReentrantLock lock = new ReentrantLock();

private transient volatile Object[] array;

写时复制Set:底层就是 CopyOnWriteArrayList

public class CopyOnWriteArraySet<E> extends AbstractSet<E>
        implements java.io.Serializable 
		
private final CopyOnWriteArrayList<E> al;

public CopyOnWriteArraySet() {
	al = new CopyOnWriteArrayList<E>();
}


public boolean add(E e) {
	return al.addIfAbsent(e);
}

public boolean addIfAbsent(E e) {
	Object[] snapshot = getArray();
	return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
		addIfAbsent(e, snapshot);
}

ConcurrentHashMap:
数据结构:数组+链表+红黑树,直接采用transient volatile HashEntry<K,V> table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率

synchronized CAS ReentrantLock

https://blog.youkuaiyun.com/sihai12345/article/details/79383766

https://www.cnblogs.com/scy251147/p/11251964.html

10、堆的结构

堆的常用方法:
1)构建优先队列
2)支持堆排序
3)快速找出一个集合中的最小值(或者最大值)

最大堆和最小堆:
在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。
这就是所谓的“堆属性”,并且这个属性对堆中的每一个节点都成立。

11、堆和树的区别
从性能上说:堆适合构建优先队列,二叉树适合搜索(需要重平衡)
从内存上说:堆内存消耗小,因为不需要构建指针

12、堆和栈在内存中的区别是什么
堆是用来存放对象的,栈是用来存放执行程序的
栈:用来存储方法变量和对象引用数据,一个栈帧的入栈和出栈就是方法的生命周期。
堆:用于存放由new创建的对象和数组。

程序运行时的内存分配有三种:静态的、堆式的、栈式的:
1)静态存储分配:指的是在编译时就能确定每个数据目标在运行时的存储空间需求,因而在编译时就给它们分配了固定的内存空间。
这种分配方式要求程序代码中不能有可变数据结构(例如可变数组)的存在,也不允许有嵌套或者递归的结构出现,
因为它们都会导致编译时编译程序无法准确计算所需的存储空间大小。

2)栈式存储分配:也可以成为动态存储分配,是由一个类似于堆栈的运行栈来实现的。和静态存储分配相反,在栈式分配方案中,
程序对于存储空间的需要在编译时是未知的,只有在运行的时候才能知道。但是规定在运行进入一个程序模块时,必须要知道该程序
模块所需的数据区大小才能为其分配内存。同我们日常了解的栈一致,栈式存储分配遵照先进后出的原则进行分配。

3)堆式存储分配:专门负责在编译时或运行时程序模块入口处都无法确定存储要求的数据结构的分配,比如可变长度串和对象实例。
堆内存由大片的可利用块或空闲块组成,堆中的内存也可以根据任意的顺序进行分配和释放。

13、什么是深拷贝和浅拷贝
eg:a = b
浅拷贝:把b的指针赋值给a,不会生成新的对象
深拷贝:生成新的对象,让a指向新的对象

implements Cloneable 重写clone()

14、讲一下对树和B+树的理解

15、LRUMap

16、如何实现一个Hashtable,你的设计如何考虑Hash冲突,如何优化(ConcurrentHashMap)

平衡树AVL:绝对的高度平衡、查找性能好、增加节点树再平衡慢
红黑树:类平衡树,不要求绝对平衡、查询性能和增删的一个折中策略
跳跃表:链表的变形、使链表具有二分查找法,随机化的数据结构(通过抛硬币来决定层数)、增删快、查询略慢于红黑树、空间代价高
B树:所有节点均存放data、空间利用率高
B+树:只有叶子节点存放data、层数少、叶子节点有序支持范围查询

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值