在算法题的刷题过程中发现集合框架和数据结构的使用息息相关,经常因为对集合框架不甚了解导致代码bug无法解决,耽误时间,遂系统学习归纳,以观成效。
1. HashMap
1.1 什么是哈希表
数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。
比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。
这个函数可以简单描述为:存储位置 = f(关键字) ,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。举个例子,比如我们要在哈希表中执行插入操作:
插入过程如下图所示
查找操作同理,先通过哈希函数计算出实际存储地址,然后从数组中对应地址取出即可。
1.2 哈希碰撞
然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。
1.3 哈希map的实现原理
1.3.1 哈希表架构(table)
HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。(其实所谓Map其实就是保存了两个对象之间的映射关系的一种集合)
1 | //HashMap的主干数组,可以看到就是一个Entry数组,初始值为空数组{},主干数组的长度一定是2的次幂。 |
Entry是HashMap中的一个静态内部类。代码如下
1 | static class Entry<K,V> implements Map.Entry<K,V> { |
所以,HashMap的总体结构如下:
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
1.3.2 其他几个重要字段
1 | /**实际存储的key-value键值对的个数*/ |
HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值
initialCapacity默认为16,loadFactory默认为0.75
1 | public HashMap(int initialCapacity, float loadFactor) { |
1.3.3 put方法
从上面这段代码我们可以看出,在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操作的时候才真正构建table数组
OK,接下来我们来看看put操作的实现
1 | public V put(K key, V value) { |
put功能总而言之:传入的key,value有旧的值,就返回就得,没有就信增一个entry,返回null
1.3.4 inflateTable方法
inflateTable这个方法用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.
1 | private void inflateTable(int toSize) { |
roundUpToPowerOf2中的这段处理使得数组长度一定为2的次幂,Integer.highestOneBit是用来获取最左边的bit(其他bit位为0)所代表的数值.
1 | private static int roundUpToPowerOf2(int number) { |
1.3.5 hash函数
1 | /**这是一个神奇的函数,用了很多的异或,移位等运算 |
以上hash函数计算出的值,通过indexFor进一步处理来获取实际的存储位置
1 | /** |
h&(length-1)保证获取的index一定在数组范围内,举个例子,默认容量16,length-1=15,h=18,转换成二进制计算为index=2。位运算对计算机来说,性能更高一些(HashMap中有大量位运算)。
1.3.6 由key到下标的最终流程
2. 字符串(String、StringBuffer、StringBuilder)
String类由final修饰,不可继承
双引号直接写的字符串在常量池中,new的不在池中!
eg:String s = new String(“xyz”);创建几个String对象?
答:2!“xyz”作为字符串文字,编译阶段进入文字池,运行时文字池并入常量池,这个池中的所有字符串常量占用一个空间(地址)。
new String则在堆上创建了一个对象,两者指向不同
1
2
3
4
5
6
7
8
9
10
11
12String str1 = "hello"; //str1指向静态区
String str2 = new String("hello"); //str2指向堆上的对象
String str3 = "hello";
String str4 = new String("hello");
System.out.println(str1.equals(str2)); //true
System.out.println(str2.equals(str4)); //true
System.out.println(str1 == str3); //true
System.out.println(str1 == str2); //false
System.out.println(str2 == str4); //false
System.out.println(str2 == "hello"); //false
str2 = str1;
System.out.println(str2 == "hello"); //true
2.1 比较
==和equals的区别
==表示内容和地址均相等,如果均为引用类型,则为同一对象
equals只表示内容相同,和hashcode有关,如果equals结果为true,则hashcode必相同;如果hashcode相同,equals结果不一定
equalsIgnoreCase(String str):忽略大小写后看是否相同
compareTo()
:两种用法- 字符串与对象进行比较。
- 按字典顺序比较两个字符串。
比较的方式:
如果第一个字符和参数的第一个字符不等,结束比较,返回第一个字符的ASCII码差值。
如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至不等为止,返回该字符的ASCII码差值。 如果两个字符串不一样长,可对应字符又完全一样,则返回两个字符串的长度差值。
1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String args[]) {
String str1 = "Strings";
String str3 = "Strings123";
int result = str1.compareTo( str3 );
System.out.println(result);//-3
result = str3.compareTo( str1 );
System.out.println(result);//3
}
}应用的题目:剑指 Offer 45. 把数组排成最小的数
2.2 获取参数
public int length()
:字符串长度public String concat(String str)
:将当前字符串和参数字符串拼接public char charAt(int index)
:获取 index处的字符(索引从0开始)public int indexOf(String str)
:查找字符串str在原字符串中第一次出现的索引值,没有返回-1
2.3 截取(substring)
public String substring(int index)
:截取从参数位置开始到字符串结尾,返回新字符串public String substring(int begin, int end)
:从begin到end,左闭右开,包含begin,不含end
2.4 转换
string转int:Integer.valueOf(“12”)||Integer.parseInt(“12”)
int转string:String.valueOf(12)
String转char:
public char[] toCharArray()
:将当前字符串拆分为字符数组作为返回值,然后用for-each遍历
char转String:
- String类:String.valueOf()
- StringBuffer类:StringBuffer.append()
1
2
3
4
5
6
7
8//String
char myChar = 'm';
String str = String.valueOf(myChar);
//StringBuffer
char myChar = 'm';
StringBuffer str = new StringBuffer();
str.append(myChar);
String s = StringBuffer.toString();String转ASCII码:
public byte[] getBytes()
:获得当前字符串底层的字节数组(Ascii值)
2.5 分割字符串
public String[] split(String regex)
:按照参数的规则,将字符串切分为若干部分1
2
3String str1 = "aaa,ccc,bbb";
String[] array1 = str1.split(",");
//注意:对于按"." 进行字符串切分,需要split("\\.")注意:碰到连续多个字符串和传入参数相匹配
此时第一个匹配的内容的中间用空字符串来表示,添加到结果数组
1
2
3
4
5
6
7public static void main(String[] args) {
String s = "a123good123123example";
String[] word = s.split("123");
for(int i=0; i<word.length; ++i){
System.out.println(word[i]);
}
}输出结果
1
2
3
4a
good
example对数组遍历进行修改
1
2
3
4for(int i=0; i<word.length; ++i){
if("".equals(word[i])) continue;
System.out.println(word[i]);
}输出结果
1
2
3a
good
example
2.6 判断
public boolean isEmpty()
:字符串长度是否为0;public boolean contains(String str)
:是否包含目标字符串;publci boolean startsWith(String str, int toffset)
:是否以指定的前缀开始publci boolean endsWith(String str)
:是否以指定的后缀结束
2.7 遍历
- toCharArray()
- .length(),charAt()
- .length(),substring(i,i+1)
2.8 增
- 直接”+”相加:str1+str2;
- StringBuffer类方法:
str1.append()
括号内可以是字符串,int或char - String类方法:
public String join(String method, String a, String b,...)
:通过某种方式实现字符串拼接
2.9 删
- 删除指定位置字符或字符串!StringBuffer类方法:
- delete(int begin, int end);
- deleteCharAt(int index);
- 删除字符串首尾的空格
- String trim():返回值为删除首尾空格后的字符串
2.10 改
- 用新字符串替换老字符串:
public String replace(CharSequence oldString, CharSequence newString)
- 大小写转换:
toLowerCase
:字符串转小写toUpperCase
:字符串转大写
2.11 查
- 对应索引位置处的字符:
str.charAt(int index)
; - 索引区间内的字符串
3 . 集合框架
3.1 List
3.1.1 lists.add(list)和lists.add(new ArrayList<>(list))的区别
1 | //第一种情况: |
总而言之,列表中存储的只是对象或者引用类型,
3.1.2 初始化ArrayList 的三种方式
常用:先创建list,再给list赋值
1
2
3
4List<String> list1 = new ArrayList<String>();
list1.add("apple");
list1.add("banana");
list1.add("orange");直接提供list值进行初始化
Arrays.aslist():数组化集合
1
List<String> list2 = new ArrayList<String>(Arrays.asList("apple", "banana", "orange"));
Collections.nCopies():复制2次
1
List<String> list3 = new ArrayList<String>(Collections.nCopies(2, "orange"));
使用匿名内部类初始化
1
2
3
4
5
6List<String> list4 = new ArrayList<>(){
{add.("apple");
add.("orange");
add.("banana")
}
};
3.1.3 ArrayList的源码
为何要设置EMPTY_ELEMENTDATA,DEFAULTCAPACITY_EMPTY_ELEMENTDATA,两个空的数组常量
数组为EMPTY_ELEMENTDATA就走基于用户设置大小值进行1.5倍扩容(这里是空所以是0),数组为默认空DEFAULTCAPACITY_EMPTY_ELEMENTDATA就会走基于默认值的大小10扩容进行1.5倍扩容。 核心代码:如果是默认初始化空容量会走 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); }这段代码,接下来基于默认初始化大小10扩容,依次是10,15,22,33这种1.5倍数扩容。如果List