常用数据结构与java集合(基于1.8版本,个人诚意满满)

0、前言

        做java开发过程中,经常会根据实际业务场景使用到很多不同的数据结构,常见的队列、栈、数组、链表、hash表等在java语言中都有对应的体现,如Stack、ArrayList、HashMap、LinkedList等;但是对于其我们可能光知道各个数据结构的特点,有时会忽略java究竟是如何实现这些结构的,本文简单对常用数据结构在java中的体现做一个学习总结。

1、顺序表

1.1、数组

// 静态初始化
int[] intArray = new int[]{1, 2, 3};

// 动态初始化,必须指定初始大小
int[] intArray = new int[5];

1.2、ArrayList集合

  • ArrayList类方法解读:

        ArrayList底层就是数组,他初始化时就默认创建了长度为10的数组;

        最大容量Integer的MAX:即2^31-1=2147483647

        添加数据的时候,add方法会使内部size+1,若是最后的size值大于当前数组的长度,则调用grow方法:

        扩容关键代码:相当于加上自身长度的一半,即扩容1.5倍

int newCapacity = oldCapacity + (oldCapacity >> 1);
  • 常用方法:
//初始化
List<Integer> list = new ArrayList<>();

//增加
list.add(1);
list.add(2);

//删除
list.remove(1);

//改
list.set(1,3);

//查
list.get(1);

//查询长度,这里返回的就是ArrayList类中名为size的属性。
list.size();

1.3、Vector集合

        同样底层是数组实现,但区别于ArrayList,它是线程安全的(synchronized)

2、链表

LinkedList集合

        ArrayList的底层实现是数组,这导致其删除一个元素的开销比较大,这里我们就想到数据结构中的链表结构了,在JAVA中有LinkedList这个类:

var list = new LinkedList<String>();

//给头部添加数据
list.addFirst("a");

//给尾部添加数据
list.addLast("c");

//获取数据
list.getFirst();
list.getLast();

//获取并移除
list.removeFirst();
list.removeLast();

System.out.println(Arrays.toString(list.toArray()));

3、栈

特点:元素先进后出

Stack类

底层是数组 ,实现自Vector,使用方法如下:

get()

由于是数组实现,所以查询元素很方便,但是vector有同步锁,线程安全,查询效率低

push()

入栈

pop()

弹出并返回

peek()

只返回不弹出

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //最大容量

LinkedList

底层是链表

addFirst()\push

入栈

removeFirst()\pop

出栈并返回

getFirst()\peek

只返回不弹出

4、队列

特点:元素先进先出

Queue

可以由LinkedList实现使用

add()

入队列

remove()

出队列

peek()

只返回不出

Deque:双端队列(也可以当做栈用)

可以由LinkedList实现使用(实现原理:添头删尾或者添尾删头)

addLast、removeFirst

入和出操作

addFirst、removeLast

getFirst\getLast

根据实际情况使用

5、树

5.1、二叉树

java实现

public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int x) {
        this.val = x;
    }
}

6、散列表(哈希表)

6.0、概念:

        个人理解,hash表就是底层基于数组实现,所有元素存储由一个hash函数计算得到结果作为地址(数组中存储的位置)

好处:不论表内数据有多少,查询效率不受影响,始终是O(1),即计算出结果直接找到位置;

  • 常用hash方法:

名称

方法

优点

缺点

直接定址法

hash(key) = a*key + b

不会冲突

连续占用空间,空间效率低

除留余数法

hash(key)=key mod p

特点:采用余数作为地址,注意根据情况设置p

乘余取整法

Hash(key)= |B( Akey mod 1 ) |

AB都是常数,mod1是为了取小数部分,然后放大B倍再取整

平方取中

比如150,平方后为22500,选其中250

折叠法

1231453233:拆成123、1453、233,比如做加法123+1453+233=1809

将长度 缩短了,宛如折叠

6.1Java中的体现:

        java中内置的对象都有自己的计算hash值得方法------hashcode()

例如:

Integer

value

Long

(int)(value ^ (value >>> 32))

Character

(int)value

Boolean

value ? 1231 : 1237

String

public int hashCode() {

int h = hash;

if (h == 0 && value.length > 0) {

char val[] = value;

for (int i = 0; i < value.length; i++) {

h = 31 * h + val[i];

}

hash = h;

}

return h;

}

        string比较牛啊:每一位上的字符的值+h*31-->h,不停地乘;原理就是通过这个方法计算值,用int来装,int的范围是【-21474836482147483647】,string的hashcode均匀分布在这里面;

        这里h会缓存起来,计算过一次后能直接用(即方法中h==0的判断意义所在

为什么31作为常数?我查到几种说法:

  • 设计的人员再经过大量(据说5万多个单词)的实验,发现使用31造成冲突的次数不超过7次;
  • 因为设计hashcode建议用质数(也有说法,可以降低冲突)作为常数,而且2^5=32,31离32最近,计算hashcode这个方法中涉及到做位运算,使用31时,能让x << 5这样的运算效率得到提升(31 * i = (i << 5) - i(左边  31*2=62,右边   2*2^5-2=62)),这里涉及到用移位和减法代替乘法,JVM计算更高效
  • 首先必须是奇数,不然2的倍数,做乘法等同于位运算,数值溢出非常容易冲突啊,其次用素数是习惯,31乘ascii码(一般也就最多100多点),通常五个字符以上(31^5)才会超出int范围.

6.2HashMap

自动扩容2倍关键源代码:默认值16,扩容是位运算左移一位

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始化默认桶的个数是16
static final int MAXIMUM_CAPACITY = 1 << 30;        // 最大容量:2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f;    // 负载因子是0.75

static final int TREEIFY_THRESHOLD = 8;            //链表元素个数达到8个,数组元素达到64才会使用红黑树
static final int MIN_TREEIFY_CAPACITY = 64;       

static final int UNTREEIFY_THRESHOLD = 6;          //红黑树元素个数小于6转化回链表

底层结构:

        1.8之前数组加链表:数组能够快速定位到是哪一组链表,然后链表优势是快速插入和删除,二者优势相结合,有效避免了链表过长查询效率低,数组过长增删效率低的问题

        1.8开始是数组加链表加红黑树:达到条件才会使用

几点疑惑解答:

        为什么不一上来就用红黑树?

        答:因为元素个数不多的情况,单个TreeNode占用空间大小比普通链表的node要大,定8这个数字,应该就是基于这个来定的。另外要知道使用到红黑树概率很小,小于千万分之一,真的我们的结构用上红黑树了,那么就需要认真考虑是不是自己实现的hash函数设计的有问题。

        为什么负载因子是0.75?

        答:源码中自有一段注释:大概经过设计人员的调研,使用0.75这个值很好的平衡是空间与时间的成本(数值越大,时间成本越大(查询效率),空间利用率高),提升hashmap的性能应当考虑再初始化时设置的初始桶数量,理想的最大条目数除以0.75后那个数值最佳。

 hashmap的hash函数:

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

解析:取hashcode然后右移16位(将低16位丢弃),再与hashcode值异或计算(相当于hashcode自己高16位与低16位做异或)然后返回;桶中具体位置计算还要拿hash方法的结果对数组长度取余。

扰动函数:(h = key.hashCode()) ^ (h >>> 16)

扰动函数及作用:扰动函数即上面提到的hash方法得到的值对桶长度(长度设置要是16的倍数,默认是16)取余,比如取到低4位1011,这和hash函数直接得到值的低4位来说随机性大大增强了(扰动函数得到的低四位有高位参与,后者直接截取低4位相当于抛弃了高位的随机性),有关人士研究发现:扰动能减少10%的hash冲突。

6.3、HashSet

        其实底层就是HashMap,初始大小16,底层一些计算特性就是hashMap的,因为同一个对象计算出来的hashcode相同,所以添加相同元素会导致后来的覆盖前面的位置,天然去重。

private static final Object PRESENT = new Object();

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

//添加元素的时候,将自己本身做为key,value全部存的Object
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

相关代码如下:

红色块:hash值算出来一样(冲突了),同时equals为true(说明是一个对象),直接覆盖。

黄色块:判断是树的话,直接走红黑树的方法添加冲突元素。

绿色块:还是链表,继续添加个节点。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿星_Alex

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值