常用数据结构
链表:你可以想象成自行车链子 一个拴着一个(data数据域和指针next组成),单向链表指针永远指向下一个,最后一个指针是null。双向链表,则有头和尾,循环链表尾指向头。
创建一个单链表的节点类
/**
* 单向链表的节点
*/
public class Node {
private Integer data; //节点中的数据
private Node next; //储存的下一个节点(每个节点都保存了本节点的data和指向下一个节点的地址)
public Node(Integer data) {
this.data = data;
}
public Integer getData() {
return data;
}
public void setData(Integer data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
方法1:添加数据到链尾
public class SingleLinkedList {
private Node head; // 创建单链表的头节点,因为链表是从头节点开始遍历,找到头节点才能顺着链子向后面遍历
private int size;//表示单链表的长度
public SingleLinkedList() {
}
public Node getHead() {
return head;
}
public int getSize() {
return size;
}
//向但链表的链尾添加数据
public boolean add (Integer data){
//创建一个你想要添加数据的节点,头空赋给头,不是空的赋给尾
Node tmp = new Node(data);
//判断头本身是否空(并不调用.next 不是判断下一个为空)
if (head==null)
head = tmp;
//若头不是空
else {
//创建临时变量,把头赋给临时指针 因为需要head移动遍历
Node t = head;
//从头开始只要后面不为空,就一直往后走
while (t.getNext()!=null){
//只要后面的不为空,就把后面的值赋给t变量,新的t继续执行循环
t = t.getNext();
}
//此时跳出循环,证明后一个为空,把你要赋值的节点给t.next(此时为空)
t.setNext(tmp);
}
//执行完毕,链表长度+1
size++;
return true;
}
}
public class SingleLinkedListTest {
public static void main(String[] args) {
//测试添加数据到队尾的add方法
SingleLinkedList list = new SingleLinkedList();
//自动装箱成Integer传入
list.add(1);
list.add(2);
list.add(3);
}
}
通过debug查看,发现每个next下都存在data和下一个next
2.指定位置添加数据
添加数据到下标位置,比如添加5到index2,为了不使原数据丢失,需要使包含data5的节点指向3的节点,然后断开2指向3的链子,使2指向5,这样指定下标的数据添加就完成了。
/**
* 添加数据到下标位置
*
* @param data 数据
* @param index 下标
* @return
*/
public boolean add(Integer data, int index) {
//创建一个你想要添加数据的节点
Node tmp = new Node(data);
//你要添加的下标大于等于数组长度
if (index >= size) {
throw new RuntimeException("越界异常!IndexOutOfBoundException,index:" + index + ",size" + size);
}
//你要在链表头插数据
else if (index == 0) {
//你的节点指向头
tmp.setNext(head);
//把你的节点赋值给头
head = tmp;
}
//在头以后插数据
else {
//创建链表头的临时变量方便进行遍历
Node t = head;
for (int i = 0; i < index - 1; i++) {
//每次循环都把下一个节点赋值给这个变量
t = t.getNext();
}
//让你要存的节点指向下一个节点
tmp.setNext(t.getNext());
//让你存入节点的上一个节点指向你存入节点
t.setNext(tmp);
}
size++;
return true;
}
/**
* 遍历链表的方法
*/
public void print() {
Node temp = head;
while (temp != null) {
System.out.println(temp.getData());
temp = temp.getNext();
}
}
3.删除最后一个数据
当要删除最后一个节点时只要把前一个连接的链子断掉,就是删除了。
/**
* 删除最后一个节点:指向最后一个节点的链子断掉
* @return
*/
public Integer remove(){
//如果头节点为空
if (head == null){
throw new RuntimeException("链表为空,不能删除");
//如果头节点的下一个为空,也就是只有头节点一个
}if (head.getNext()==null){
//获取头节点的data
Integer data = head.getData();
//给头节点赋空值
head = null;
//返回删除的数据
return data;
}
//定义两个指针(tSlow在前tFast在后),满足条件就同时移动,
// 直到fast为空,也就是说fast后面没有节点了
Node tSlow =null;
Node tFast =head;
//只要快指针后面有节点就一直遍历
while (tFast.getNext()!=null){
//两个指针同时向后移动
tSlow = tFast;
tFast = tSlow.getNext();
}
//循环结束时,证明快指针后面为空,即快指针为最后一个节点
//删除最后一个节点,只需要把前面和他相连的链子断了就行
tSlow.setNext(null);
//返回删除的数据(节点尾)
return tFast.getData();
}
4.删除指定下标的节点
/**
* 根据下标删除节点:index前后连起来
* @param index
* @return
*/
public Integer remove(Integer index){
if (index >= size){
throw new RuntimeException("越界异常!IndexOutOfBoundException,index:" + index + ",size" + size);
}if (index == 0){
//获取头节点的data
Integer data = head.getData();
//删除后的头节点就是下一个节点了
head = head.getNext();
//返回删除的数据
return data;
}
//要删除的下标不是0时,说明在头以后
//定义两个指针(tSlow在前tFast在后),满足条件就同时移动,
Node tSlow =null;
Node tFast =head;
//当循环到下标index时,最终tSlow指向你要删除的节点,tFast是index指向下个节点的指针
for (int i = 0; i <index ; i++) {
//两个指针向后移动
tSlow = tFast;
tFast =tFast.getNext();
}
//使指向index的指针指向index的下一个
tSlow.setNext(tFast.getNext());
//使index指向下个节点的指针为null,这样index的前一个和index的后一个就连起来了
tFast.setNext(null);
return tFast.getData();
}
5.通过下标获取链表指定数据
/**
* 通过下标获取链表指定数据
* @param index
* @return
*/
public Integer get(int index){
if (index<0||index>=size){
throw new RuntimeException("越界异常!IndexOutOfBoundException,index:" + index + ",size" + size);
}
Node temp = head;
for (int i = 0; i <index ; i++) {
temp =temp.getNext();
}
return temp.getData();
}
二叉树:
二叉树是树的一种,每个节点最多可具有两个子树,即结点的度最大为 2(结点度:结点拥 有的子树数)。
1号位于根节点,其余叫做子节点,每个节点相当于被分成三部分,一份存放自身的数据,另两份存放左右两个节点的地址。
创建二叉树节点
/**
* 二叉树的节点
*/
public class TreeNode {
//数据
private Integer data;
//左节点 默认null
private TreeNode left;
//右节点 默认null
private TreeNode right;
public TreeNode(Integer data) {
this.data = data;
}
public Integer getData() {
return data;
}
public void setData(Integer data) {
this.data = data;
}
public TreeNode getLeft() {
return left;
}
public void setLeft(TreeNode left) {
this.left = left;
}
public TreeNode getRight() {
return right;
}
public void setRight(TreeNode right) {
this.right = right;
}
}
创建一颗树
public static void main(String[] args) {
//创建7个节点
TreeNode root = new TreeNode(1);
TreeNode node2 = new TreeNode(2);
TreeNode node3 = new TreeNode(3);
TreeNode node4 = new TreeNode(4);
TreeNode node5 = new TreeNode(5);
TreeNode node6 = new TreeNode(6);
TreeNode node7 = new TreeNode(7);
//这些节点构建为一棵树
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
node3.setRight(node7);
}
广度优先遍历:按层遍历
/**
* 广度优先遍历:按层打印,先父后子,取出父的时候子进行排队,子取出,子的子再排队以此类推
* @param root
*/
public static void printByLayer(TreeNode root){
//根节点为空直接return
if (root==null){
return;
}
//创建一个队列存储节点 队列特点:先进先出
Queue<TreeNode> queue = new LinkedList<>();
//把根节点存入队列
queue.add(root);
//只要队列里面有东西,就一直循环
while (!queue.isEmpty()){
//从队列中取出一个节点赋值给临时变量
TreeNode temp = queue.poll();//弹出数据
//打印这个数据
System.out.println(temp.getData());
//开始管理自己的子节点
if (temp.getLeft()!=null)
queue.add(temp.getLeft());
if (temp.getRight()!=null)
queue.add(temp.getRight());
}
}
深度优先遍历
/**
* 前序遍历:根左右
*
* @param root
*/
public static void pre_print(TreeNode root) {
if (root!=null){
//根
System.out.println(root.getData());
//左
if (root.getLeft()!=null)
pre_print(root.getLeft());
//右
if (root.getRight()!=null)
pre_print(root.getRight());
}
}
/**
* 中序遍历:左根右
*
* @param root
*/
public static void mid_print(TreeNode root) {
if (root!=null){
//左
if (root.getLeft()!=null)
mid_print(root.getLeft());
//根
System.out.println(root.getData());
//右
if (root.getRight()!=null)
mid_print(root.getRight());
}
}
/**
* 后序遍历:左右根
*
* @param root
*/
public static void last_print(TreeNode root) {
if (root!=null){
//左
if (root.getLeft()!=null)
last_print(root.getLeft());
//右
if (root.getRight()!=null)
last_print(root.getRight());
//根
System.out.println(root.getData());
}
}
二叉排序树:
/**
* 二叉排序树
*/
public class BinarySortTree {
//定义根节点
private TreeNode root;
/**
* 二叉排序树的添加 :根据根节点比较小于根节点放左边,大于根节点放右边
* 注意点一:如果插入的值,树中已经存在,则不会继续插入
* 注意点二:遍历要采用中序遍历(左根右->小中大)
* @param data
* @return
*/
public boolean add(Integer data){
if (root == null){
this.root = new TreeNode(data);
}else {
//定义当前节点
TreeNode current = root;
//定义相对的根节点
TreeNode parentNode = null;
//只要当前节点不为空就一直循环
while (current!=null){
//把当前节点作为根节点
parentNode = current;
//传入的数据小于当前节点
if (data < current.getData()){
//把当前节点的左侧当作当前节点
current = current.getLeft();
if (current == null){
parentNode.setLeft(new TreeNode(data));
return true;
}
}
//传入的数据大于当前节点
else {
//把当前节点的右侧当作当前节点
current = current.getRight();
if (current == null){
parentNode.setRight(new TreeNode(data));
return true;
}
}
}
}
return false;
}
public TreeNode getRoot() {
return root;
}
public void setRoot(TreeNode root) {
this.root = root;
}
}
测试及结果(采用中序遍历)
查找指定数据(get)
/**
* 查询树中有没有对应的值
* @param key
* @return
*/
public TreeNode get(Integer key){
TreeNode temp = root;
while (temp!=null){
if (temp.getData()>key){
temp = temp.getLeft();
}else if(temp.getData()<key){
temp = temp.getRight();
}else {
return temp;
}
}
return null;
}
若设二叉树的深度为 h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树
栈:
队列:
数组:
集合和数组既然都是容器,它们有啥区别呢?
数组的长度是固定的。集合的长度是可变的。
数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类
型可以不一致。在开发中一般当对象多的时候,使用集合进行存储
类集设置的目的
对象数组有那些问题?普通的对象数组的最大问题在于数组中的元素个数是固定的,不能动态的扩充大小,所以最 早的时候可以通过链表实现一个动态对象数组。但是这样做毕竟太复杂了,所以在 Java 中为了方便用户操作各个数据结构, 所以引入了类集的概念,有时候就可以把类集称为 java 对数据结构的实现。
在整个类集中的,这个概念是从 JDK 1.2(Java 2)之后才正式引入的,最早也提供了很多的操作类,但是并没有完 整的提出类集的完整概念。
类集中最大的几个操作接口:Collection、Map、Iterator,这三个接口为以后要使用的最重点的接口。
所有的类集操作的接口或类都在 java.util 包中。
Java 类集结构图:
Collection 接口
Collection 接口是在整个 Java 类集中保存单值的最大操作父接口,里面每次操作的时候都只能保存一个对象的数据。 此接口定义在 java.util 包中。C ollection一共定义了15个方法(比如迭代器,size,add),但我们实际开发中并不会直接去操作Collection,而是去操作他的子接口(List和Set)。
常用方法:
。 public boolean add(E e) : 把给定的对象添加到当前集合中
。 public void clear() :清空集合中所有的元素
。 public boolean remove(E e) : 把给定的对象在当前集合中删除
。 public boolean contains(E e) : 判断当前集合中是否包含给定的对象
。 public boolean isEmpty() : 判断当前集合是否为空
。 public int size() : 返回集合中元素的个数
。 public Object[] toArray() : 把集合中的元素,存储到数组中
。没有get方法,ArrayList和Vector有
Iterator
迭代器,区别于以前的遍历是系统迭代数据结构的最优实现,每个类集都会自带一个迭代器。
List接口
List也是保存单值的借口,同时对于保存的数据规定是有序,可重复的。
在 List 接口中有以上 10 个方法是对已有的 Collection 接口进行的扩充(比如拥有自己的迭代器ListIterator)。我们平时使用List接口的时候则是针对其实现类(ArrayList,Vector,LinkedList)进行操作。
ArrayList
传统数组虽然取值比较快速,但是对于增加或者修改尤其缓慢。数组一旦创建则不可改变,如果我们添加的值超过了数组的界限,则我们需要手动扩容(非常麻烦,且耗费内存)。对于删除输出来说,删除一个数据需要后面的所有数据往前位移,如果是一百万个数据呢???
使用ArrayList则完美的解决了这个问题,相比于数组,ArrayList是可自动扩容的数组,我们无须再手动扩容。底层数据结构依然是数组结构array,所以查询速度快,增删改慢。
通过源码分析其实现,ArrayList可以通过构造方法在初始化的时候指定底层数组的大小。
通过无参构造方法的方式ArrayList()初始化,则赋值底层数Object[] elementData为一个默认空数组Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}所以数组容量为0,只有真正对数据进行添加add时,才分配默认DEFAULT_CAPACITY = 10的初始容量。
大家可以分别看下他的无参构造器和有参构造器,无参就是默认大小,有参会判断参数。
ArrayList 会自动判断传入长度,当数组长度不够的时候就会自动扩容
jdk1.8以后大概是这个样子
原数组容量右移一位,此操作在二进制的基础上位移动。举个例子十进制右移一位(10就 变成了1,相当于除10),二进制(正数)则是最右位去掉,最高位补0,则相当于除以二。
我们看这个公式就是 新长度 = 旧长度+0.5旧长度。 所以 ArraysList每次扩容都是之前的长度都是之前的1.5倍。
同时ArraysList是线程不安全的,Vector 是线程安全的。
ArrayList和Vector相同点与区别:
同:
1 ArrayList和Vector都是继承了相同的父类和实现了相同的接口 2 底层都是数组(Object[])实现的
3 初始默认长度都为10。
区别:
1 同步性:
Vector中的public方法多数添加了synchronized关键字、以确保方法同步、也即是Vector线程安全、ArrayList线程不安全。
2 扩容:
ArrayList以1.5倍的方式在扩容、Vector
当扩容容量增量大于0时、新数组长度为原数组长度+扩容容量增量、否则新数组长度为原数组长度的2倍3性能:
在性能方便通常情况下ArrayList的性能更好、而Vector存在synchronized
的锁等待情况、需要等待释放锁这个过程、所以性能相对较差。4 输出:
ArrayList支持支持 Iterator、ListIterator 输出,Vector除了支持
Iterator、ListIterator外,还有Enumeration输出
ArrayList常用方法
List接口常用方法:
1、add(Object element): 向列表的尾部添加指定的元素。
2、size(): 返回列表中的元素个数。
3、get(int index): 返回列表中指定位置的元素,index从0开始。
4、add(int index, Object element): 在列表的指定位置插入指定元素。
5、set(int i, Object element): 将索引i位置元素替换为元素element并返回被替换的元素。
6、clear(): 从列表中移除所有元素。
7、isEmpty(): 判断列表是否包含元素,不包含元素则返回 true,否则返回false。
8、contains(Object o): 如果列表包含指定的元素,则返回 true。
9、remove(int index): 移除列表中指定位置的元素,并返回被删元素。
10、remove(Object o): 移除集合中第一次出现的指定元素,移除成功返回true,否则返回false。
11、iterator(): 返回按适当顺序在列表的元素上进行迭代的迭代器。
ArrayList一样可以使用List的所有方法
Vector
线程安全,因为公开的方法都加了锁,同时导致了性能不好。和ArraysList一样使用了数组的结构,查找快,增删慢。ArrayList有的方法他都有。
LinkedList
使用的是双向链表,增删快,查找慢。
存储的结构是链表结构 List里面的方法多有 同时还有自己特有的
。public void addFirst(E e) :将指定元素插入此列表的开头
。 public void addLast(E e) :将指定元素添加到此列表的结尾
。 public E getFirst() :返回此列表的第一个元素
。 public E getLast() :返回此列表的最后一个元素
。 public E removeFirst() :移除并返回此列表的第一个元素
。 public E removeLast() :移除并返回此列表的最后一个元素
。 public E pop() :从此列表所表示的堆栈处弹出一个元素
。 public void push(E e) :将元素推入此列表所表示的堆栈
。 public boolean isEmpty() :如果列表不包含元素,则返回true。
方法详见:https://blog.csdn.net/vjrmlio/article/details/7950887#
Iterator接口
用来遍历Collcection集合下的所有集合 List Set…
public E next() :返回迭代的下一个元素 同时指针下移。
public E previous() :返回迭代的上一个元素 同时指针上移。
public boolean hasNext() :如果仍有元素可以迭代,则返回 true
增强for
for(数据类型 变量名 : 集合 或 数组名){}
失败:遍历的时候,改变了集合,导致遍历失败
快速失败:创建迭代器后的任何时间,除了自己的remove以外修改集合,迭代器将抛出ConcuurentModificationException.
安全失败:把集合复制了一份,我们遍历的是复制的,所以怎么操作无所谓,通常使用的是安全失败。
使用迭代器遍历集合的内容:
1.通过调用集合的Iterator()方法,获取指向集合开头的迭代器。
2.建立一个hasNext()方法调用循环。只要hasNext()方法返回true,就继续迭代。
3.在循环中,通过调用next()方法获取每个元素。
package Collection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;
public class IteratorDemo {
public static void main(String[] args) {
//create an Array list
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("q");
arrayList.add("e");
arrayList.add("fg");
arrayList.add("iu");
arrayList.add("ug");
System.out.println("Original contents of arraylist: ");
//创建ArrayList的迭代器
Iterator<String> iterator = arrayList.iterator();
//iterator.hasNext()判断指针指向的下一个是否为空,返回值是boolean
while (iterator.hasNext()){
//iterator.next()获取元素并且指针指向后移
String element = iterator.next();
System.out.print(element +" ");
}
System.out.println();
//Modify objects being iterated
ListIterator<String> lit = arrayList.listIterator();
while (lit.hasNext()){
String element = lit.next();
lit.set(element + "+");
}
System.out.println("Modified contents of arraylist: ");
iterator = arrayList.iterator();
while (iterator.hasNext()){
String element = iterator.next();
System.out.print(element + " ");
}
System.out.println();
//Now,display the list backwards.
System.out.println("Modified list backwards: ");
while (lit.hasPrevious()){
String element = lit.previous();
System.out.print(element + " ");
}
System.out.println();
}
}
ListIterator
介绍一下新来的几个方法:
void hasPrevious() 判断游标前面是否有元素;
Object previous() 返回游标前面的元素,同时游标前移一位。游标前没有元素就报 java.util.NoSuchElementException的错,所以使用前最好判断一下;
int nextIndex() 返回游标后边元素的索引位置,初始为 0 ;遍历 N 个元素结束时为 N;
int previousIndex() 返回游标前面元素的位置,初始时为 -1,同时报
java.util.NoSuchElementException 错;
void add(E) 在游标 前面 插入一个元素 注意是前面
void set(E) 更新迭代器最后一次操作的元素为 E,也就是更新最后一次调用 next() 或者 previous() 返回的元素。 注意,当没有迭代,也就是没有调用 next() 或者 previous() 直接调用 set 时会报错
void remove() 删除迭代器最后一次操作的元素,注意事项和
set 一样。
forEach
forEach:增强for循环,最早出现在C#中
用于迭代数组 或 集合 (Collection下的集合才行list和set)(自动寻在最优解)
for(数据类型 :变量名 :集合或者数组名){
// 变量一只在循环中改变
System.out.println(变量名)
}
Set(接口)
继承自Collection,方法几乎一致,并没有太多的改变。和Collection一样没有get方法,只有用toArray变成数组或者iterator迭代器找数据,这是为什么呢?因为Set的实现类不包含重复单值的集合,及不满足e1.equals(e2) 且 null 也最多就一个。
如果使用可变对象作为set元素则需要非常小心,比如传入一个Person,里面有属性name age 结果被改变了 ,位置也就改变。
HashSet
没有实现过多自己的方法,基本还是用Colletion的方法,没有get,只能toArray变成数组操作或者迭代器操作。内部是散列存放,基于hashmap存储。
换个方式理解,我们编写一个软件,存储数据,长度不确定,用数组不合适,选择集合,不增删只查找,我们就可以用ArrayList,我们使用,系统已经提供了动态扩容结构。 设计哈希表,hashset是单值, 之前map已经是双值存储的hash表了 ,系统怎么设计呢, 就利用了这个hash表
put 数据 一定要是两个 一个是我们传入的 一个是写死的
add方法返回的是boolean类型,基于set无序的不可重复的特点,因为存在所以添加失败为false,因为不可重复所以输出以下结果。至于如何实现无序和不可重复的呢?详见后文hashmap
TreeSet和Comparable
TreeSet内部也是用treemap实现的,Hashset无序,TreeSet有序(自然顺序,不是根据你输入的顺序,系统根据Ascall码排序)
注意:如果想根据你自定义的顺序进行排序输出,要在类型的类中实现Compareble<类型>接口,否则直接add会抛异常,如图。
类型转换异常,因为系统也不知道怎么给你排,你需要实现接口重写里面的compareTo方法。
//实现Comparable,写要比较的类型
static class Person implements Comparable<Person>{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
name.equals(person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public int compareTo(Person o) {
//this 与 泛型 传入的o比较
//返回的数据:负数:this小/零 一样/正数 this 大
//里边写重写的方法
return 0;
}
// Person p1 = new Person("张三",18);
// Person p2 = new Person("李四",19);
// data.add(p1);
// data.add(p2);
// p1存的时候不排序,P2存的时候,就相当于p1.comPareTo(p2)
//根据返回的数据决定谁大谁小存在不同的位置
@Override
public int compareTo(Person o) {
if(this.age>o.age){
return 1;
}else if (this.age == o.age){
return 0;
}
return -1;
}
}
自定义排序规则后的输出(根据自身业务需要)
但是如果我们添加了一个添加了一个18岁的王二麻子会怎么样?
我们发现王二麻子并没有输出,因为set是不能重复的。
Compartor接口
https://blog.csdn.net/lx_nhs/article/details/78871295
Map
注意:List(单值),Set(单值),Map(双值) 并不在同一等级。Map和Collection是同一个级别,只不过我们很少用Collection。
Map集合存储的是一个个的 键值对 数据。
左边是像list set单值存储结构,右边是map双值结构。
左边通过迭代器方式或下标方式获取元素,右边则根据key获取value。
之前说hashset内部是hashmap实现的,treeset是用treemap实现的,这是为什么?
相当于利用了map的key(不可重复性)来存储数据。
右边的已经被写死,左边利用map的k来存储数据。
map不是地图的意思(谷歌翻译有误),是映射的意思(mapping的缩写),每个键只能映射一个值。因为键和值都不是有序的,根据我们定义的,所以我们无法通过遍历得到他们的值。我们可以把key都保存在keyset里面,再遍历keyset()得到所有的key,然后再调用get方法传入key取出value; put方法存值
如图,为什么put(k,v)的时候需要返回值呢?
因为map不允许存重复的值所以当我们put一个k一样的值但是v不一样的值的时候,系统会用新值替换旧的值,就会把旧值返回回来。如果没有替换,只是单纯的添加,那就会返回null。
remove为什么也返回v?
删除成功返回的就是删除的值,删除失败就返回null。我们有时候也用remove取出数据,如果需要取出并删除可以使用remove
size() 获取到键值对的数量
哈希表
数组是我们平时常见的并且经常使用的一种数据结构,那么它具有什么优点呢?我们都知道,在我们知道数组中某元素的下标值时,我们可以通过下标值的方式获取到数组中对应的元素,这种获取元素的速度是非常快的。
但是呢,数组也是有一定的缺点的,如果我们不知道某个元素的下标值,而只是知道该元素在数组中,这时我们想要获取该元素就只能对数组进行线性查找,即从头开始遍历,这样的效率是非常低的,如果一个长度为10000的数组,我们需要的元素正好在第10000个,那么我们就要对数组遍历10000次,显然这是不合理的。
所以,为了解决上述数组的不足之处,引入了哈希表的概念,哈希表在很多语言的底层用到的非常的多
哈希表的优缺点
在刚才哈希表的描述中,大家一定能看出哈希表的优缺点了,这里我来给大家总结一下吧~
(1)优点
首先是哈希表的优点:
无论数据有多少,处理起来都特别的快
能够快速地进行 插入修改元素 、删除元素 、查找元素 等操作
代码简单(其实只需要把哈希函数写好,之后的代码就很简单了)
(2)缺点
然后再来讲讲哈希表的缺点:
哈希表中的数据是没有顺序的
数据不允许重复
(3)冲突
前面提到了冲突,其含义就是在哈希化以后有几个元素的下标值相同,这就叫做 冲突。 那当两个元素的下标值冲突时,是后一个元素是不是要替换掉前一个元素呢?当然不是!
那么如何解决冲突这个现象呢?一般是有两种方法,即拉链法(链地址法) 和 开放地址法
拉链法:如果存的都在同一个位置就使用链表,一个绑着一个
开放地址法:如果存的过多就使用红黑树
实例:
哈希表使用的是对象数组+链表,或者对象数组+红黑树。
比如Hashmap默认初始长度为16的对象数组+链表结构,下标是0-15,当我们存入一个数据时,先通过hashCode()计算其hash值(返回一个int),然后对其长度(桶数量)(此时是16求余%得到一个0-15下标的数)根据该数找到对应的哈希桶(位置)。那么如果计算出来比如17%16=1 33%16=1,两个数值一样怎么办?哈希冲突:有两种解决办法,这里我们只用开放地址法。
当第二次计算33%16得出1的时候,查看2下标的位置有没有,如果有就继续以此类推。。。
Hash表、Hash函数和HashCode
jdk1.8以后针对哈希表进行了优化,当哈希桶达到8时候,转换为红黑二叉树进行存储。当哈希桶数量减少到6的时候,从红黑二叉树转换为链表。
假设我们存了一万个数据,那么这16个桶(无法确定初始值,就设置16),每一个桶都有大几百个数据,这样性能肯定会变的很慢。
散列因子:0.75 当16个桶中有百分之七十五存上数据了就对桶扩容,桶就会更多,默认长度是桶的两倍。
那么下次取余就是32了 比如:322%32
HashMap源码分析
散列因子(默认0.75,也可以通过有参构造修改,官方最推荐0.75)
默认容量16,1左移四位
put方法
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;
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;
}
冲突:链表或红黑树解决
HashMap/Hashtable/ConcurrentHashMap
HashMap 线程不安全的,效率高
Hashtable 线程安全的,效率低
ConcurrentHashMap
采用分段锁机制,保证线程线程安全,效率又比较高
在链表内分段排队,举例(0号位置A在执行,B计算出来去1号位置执行,遇到同一个位置的在排队,没有就去对应位置执行)
散列表的几点说明
散列因子(默认0.75):太小耗费空间,但是效率高(比如0.1当有十分之一个桶装了就扩容)
太大不耗费空间,但效率太低(比如0.9但有十分之九装了才扩容,很可能一个桶已经上千条数据了)
容量(桶的个数):默认16 比如要存10000 数据,产生了大量散列 ,初始容量一定要给的合理,本身hash表就是存取效率很高,不要因为人为操作失误导致效率变慢。
存储在hashmap的key如果是自定义类型就不要修改值
使用map,尤其是hashmap 一定要支持equals 和hashcode
当我们在get方法前修改key值时,由于get方法再调用的时候会计算hash值导致计算出来的hash值和之前不一样,就去了错误的桶里面找,当然找不到,结果为null。
此时你想通过原来的key找到value发现,这是为什么?明明我的key一样啊,却找不到value。原来hashmap在get()时不仅会调用hashcode计算hash值(key),还会调用equals判断。
所以,当我们在使用map,尤其是hashmap的时候,当作为哈希表的key存储时key就不要去改变,如果需要改变的就不要放在key上
Jdk9集合类的新特性
只对List Set Map 使用,子类不行
只能创建固定长度的集合,不可改变,不可以add,remove,set不能修改
//Set
Set<String> set = Set.of("锄禾日当午","汗滴禾下土","谁知盘中餐","粒粒皆辛苦");
for (String s : set) {
System.out.println(s);
}
//List
List<Integer> list = List.of(1,2,3,4);
for (Integer integer : list) {
System.out.println(integer);
}
//Map
Map<String,String> map = Map.of("1","锄禾日当午","2","汗滴禾下土","3","谁知盘中餐","4","粒粒皆辛苦");
Set<String> keySet = map.keySet();
for (String key: keySet) {
System.out.println(key+"->"+map.get(key));
}
最后再来个全家福