目录
一、数组
二、List
三、栈(Stack)
四、队列(Queue)
五、集合(Set)
六 、哈希表(Map)
七、各遍历方式的适用于什么场合?
八、数据元素是怎样在内存中存放的?
简介:本文只为对java常用数据结构的特点和常用方法做总结。欢迎评论留言,文章持续更新优化
一、数组
概念:数组是相同类型数据的有序集合。数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问它们。
基本特点:
- 数组中的元素在内存中连续存储的,可以根据是下标快速访问元素,因此查询速度很快。
- 其长度是确定的。数组一旦被创建,它的大小就是不可以改变的。插入和删除时,需要扩容并且对元素移动空间,比较慢。
- 其元素必须是相同类型,不允许出现混合类型,
- 数组中的元素可以是任何数据类型 ,包括基本类型和引用类型,
- 数组变量属引用类型,数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量。数组本身就是对象,Java中对象是在堆中的,因此数组无论保存原始类型还是其他对象类型,数组对象本身是在堆中的。
使用场景:
频繁查询,很少增加和删除的情况。
代码实现
/**
* 数组
* 特点:我们都知道数组中的元素在内存中连续存储的,可以根据是下标快速访问元素,
* 因此,查询速度很快,然而插入和删除时,需要对元素移动空间,比较慢。
*
* 使用场景:频繁查询,很少增加和删除的情况。
*/
//一维数组
int[] sourceArray = new int[6];
System.out.println("一位数组的长度 length = " + sourceArray.length);
System.out.println("----------------------------------");
//二维数组
int[][] sourceArray1 = new int[2][3];
//行数
int rows = sourceArray1.length;
//列数
int columns = sourceArray1[0].length;
System.out.println("二维数组行数 rows = " + rows);
System.out.println("二维数组列数 columns = " + columns);
System.out.println("----------------------------------");
二、List
常用实现包括:
1.AarryList:通过数组实现,属于动态数组。因为是通过数组实现,每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以插入和删除慢,查询速度快。
2.LinkedList:通过链表实现。查询慢,插入和删除快。适合少查询,需要频繁的插入或删除的场景。
链表的特点如下:
元素可以不连续内存中,是以索引将数据联系起来的,当查询元素的时候需要从头开始查询,所以效率比较低,然而添加和删除的只需要修改索引就可以了。
/**
* List
*/
List<String> stringList = new ArrayList<>();
//添加一个元素
stringList.add("第一个元素");
//把元素插入到某个位置上
stringList.add(0, "第零个元素");
for (String str : stringList) {
System.out.println(str);
}
System.out.println("----------------------------------");
//返回列表长度
int length = stringList.size();
System.out.println("列表长度 = " + length);
System.out.println("----------------------------------");
//判断是否包含某个元素
if (stringList.contains("第一个元素")) {
System.out.println();
}
System.out.println("----------------------------------");
List<String> jihe = new ArrayList<>();
jihe.add("第二个元素");
jihe.add("第三个元素");
//添加一个集合,实现自Collection接口的都可以添加
stringList.addAll(jihe);
List<String> jihe1 = new ArrayList<>();
jihe1.add("第负二个元素");
jihe1.add("第负一个元素");
//从某个位置开始插入集合
stringList.addAll(0, jihe1);
for (String str : stringList) {
System.out.println(str);
}
System.out.println("----------------------------------");
//返回List中与传入的字符串相等的第一个元素的下标
int firstIndexOf = stringList.indexOf("第一个元素");
System.out.println("字符串 第一个元素在 List中的位置 = " + firstIndexOf);
System.out.println("----------------------------------");
//先在队尾插入一个一样的元素
stringList.add("第一个元素");
//返回List中与传入的字符串相等的最后一个元素的下标
int lastIndexOf = stringList.lastIndexOf("第一个元素");
System.out.println("字符串 第一个元素在 List中的位置 = " + lastIndexOf);
//移除指定下标的元素
stringList.remove(0);
//移除指定值元素
stringList.remove("第一个元素");
for (String str : stringList) {
System.out.println(str);
}
System.out.println("----------------------------------");
三、栈(Stack)
概述:Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop 方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。
代码实现
/**
* Stack
* 特点:先进后出,就像一个箱子。
* 使用场景:实现递归以及表示式。
*/
Stack<String> stringStack = new Stack<>();
//添加元素,等同于add
stringStack.push("第一个元素");
stringStack.push("第二个元素");
//插入元素到指定位置,需要注意的插入的位置大小不能大于栈的长度
stringStack.add(1, "第三个元素");
//返回元素在stack中的位置
int position = stringStack.search("第二个元素");
System.out.println("第二个元素在栈中的位置 = "+position);
System.out.println("----------------------------------");
//遍历栈
while (!stringStack.isEmpty()) {
System.out.println(stringStack.pop());
}
四、队列(Queue)
概述:
队列是一种先进先出(FIFO)的抽象数据结构,在Java中,队列使用了两种数据类型来实现的,分别是:数组和链表这两种数据结构。
代码实现
/**
* 队列 queue
* 特点:先进先出。
* 使用场景:多线程阻塞队列管理非常有用。
*/
Queue<String> queue = new LinkedList<>();
queue.add("第一个元素");
queue.offer("第二个元素");
queue.offer("第三个元素");
System.out.println("队列中第一个元素 = " + queue.peek());
System.out.println("队列中第一个元素 = " + queue.element());
System.out.println("队列中第一个元素 = " + queue.poll());
//移除掉队列中第一个元素
queue.remove();
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
五、集合(Set)
Set是一种不包括重复元素的Collection。它维持它自己的内部排序,所以随机访问没有任何意义。与List一样,它同样允许null的存在但是仅有一个。由于Set接口的特殊性,所有传入Set集合中的元素都必须不同,同时要注意任何可变对象,如果在对集合中元素进行操作时,导致e1.equals(e2)==true,则必定会产生某些问题。Set接口有三个具体实现类,分别是散列集HashSet、链式散列集LinkedHashSet和树形集TreeSet。
Set是一种不包含重复的元素的Collection,无序,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。需要注意的是:虽然Set中元素没有顺序,但是元素在set中的位置是由该元素的HashCode决定的,其具体位置其实是固定的。
1.HashSet
HashSet 是一个没有重复元素的集合。它是由HashMap实现的,不保证元素的顺序(这里所说的没有顺序是指:元素插入的顺序与输出的顺序不一致),而且HashSet允许使用null 元素。HashSet是非同步的,如果多个线程同时访问一个哈希set,而其中至少一个线程修改了该set,那么它必须保持外部同步。 HashSet按Hash算法来存储集合的元素,因此具有很好的存取和查找性能。
HashSet的实现方式大致如下,通过一个HashMap存储元素,元素是存放在HashMap的Key中,而Value统一使用一个Object对象。
HashSet使用和理解中容易出现的误区:
a.HashSet中存放null值
HashSet中是允许存入null值的,但是在HashSet中仅仅能够存入一个null值。
b.HashSet中存储元素的位置是固定的
HashSet中存储的元素的是无序的,这个没什么好说的,但是由于HashSet底层是基于Hash算法实现的,使用了hashcode,所以HashSet中相应的元素的位置是固定的。
c.必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。
2.LinkedHashSet
LinkedHashSet继承自HashSet,其底层是基于LinkedHashMap来实现的,有序,非同步。LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
3.TreeSet
TreeSet是一个有序集合,其底层是基于TreeMap实现的,非线程安全。TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。当我们构造TreeSet时,若使用不带参数的构造函数,则TreeSet的使用自然比较器;若用户需要使用自定义的比较器,则需要使用带比较器的参数。
注意:TreeSet集合不是通过hashcode和equals函数来比较元素的.它是通过compare或者comparaeTo函数来判断元素是否相等.compare函数通过判断两个对象的id,相同的id判断为重复元素,不会被加入到集合中。
/**
* Set 集合
*/
Set<String> stringSet = new HashSet<>();
System.out.println("add result = " + stringSet.add("第一个元素"));
//第二次add 同样的元素返回结果为false
System.out.println("add result = " + stringSet.add("第一个元素"));
System.out.println("add result = " + stringSet.add("第二个元素"));
System.out.println("add result = " + stringSet.add("第三个元素"));
//foreach 遍历
for (String str : stringSet) {
System.out.println(str);
}
//通过迭代器遍历
Iterator iterator = stringSet.iterator();
while ((iterator.hasNext())) {
System.out.println(iterator.next());
}
//for循环遍历
for (Iterator itera = stringSet.iterator(); itera.hasNext(); ) {
System.out.println(itera.next());
}
六 、哈希表(Map)
概述:
Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。同时它也没有继承Collection。在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。
1.HashMap
以哈希表数据结构实现,查找对象时通过哈希函数计算其位置,它是为快速查询而设计的,其内部定义了一个hash表数组(Entry[] table),元素会通过哈希转换函数将元素的哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,可能通过查看HashMap.Entry的源码它是一个单链表结构。Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn),JDK 1.7 的 HashMap 是基于数组 + 链表实现,所以 hash 冲突时链表的查询效率低。hash(Object key) 方法的具体算法是 (h = key.hashCode()) ^ (h >>> 16),经过这样的运算,让计算的 hash 值分布更均匀
2.LinkedHashMap
LinkedHashMap是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。
LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用get方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。
注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
由于LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能,但在迭代访问Map里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。
3.TreeMap
TreeMap 是一个有序的key-value集合,非同步,基于红黑树(Red-Black tree)实现,每一个key-value节点作为红黑树的一个节点。TreeMap存储时会进行排序的,会根据key来对key-value键值对进行排序,其中排序方式也是分为两种,一种是自然排序,一种是定制排序,具体取决于使用的构造方法。
自然排序:TreeMap中所有的key必须实现Comparable接口,并且所有的key都应该是同一个类的对象,否则会报ClassCastException异常。
定制排序:定义TreeMap时,创建一个comparator对象,该对象对所有的treeMap中所有的key值进行排序,采用定制排序的时候不需要TreeMap中所有的key必须实现Comparable接口。
TreeMap判断两个元素相等的标准:两个key通过compareTo()方法返回0,则认为这两个key相等。
如果使用自定义的类来作为TreeMap中的key值,且想让TreeMap能够良好的工作,则必须重写自定义类中的equals()方法,TreeMap中判断相等的标准是:两个key通过equals()方法返回为true,并且通过compareTo()方法比较应该返回为0。
/**
* Map集合
*/
Map<String, String> stringMap = new HashMap<>();
System.out.println(stringMap.put("1", "第一个元素"));
System.out.println(stringMap.put("2", "第二个元素"));
System.out.println(stringMap.put("3", "第三个元素"));
System.out.println(stringMap.put("4", "第四个元素"));
System.out.println(stringMap.put("5", "第五个元素"));
//foreach直接遍历Map 的 entrySet()
for (Map.Entry<String, String> entry : stringMap.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
}
//单独遍历key集合
for (String str : stringMap.keySet()) {
System.out.println(str);
}
//单独遍历value集合
for (String str : stringMap.values()) {
System.out.println(str);
}
//通过迭代器遍历
Iterator<Map.Entry<String, String>> iterator = stringMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
System.out.println(entry.getKey());
System.out.println(entry.getValue());
}
七、各遍历方式的适用于什么场合?
1、传统的for循环遍历,基于计数器的:
顺序存储:读取性能比较高。适用于遍历顺序存储集合。
链式存储:时间复杂度太大,不适用于遍历链式存储的集合。
普通for循环中若将list的长度声明为临时变量使用效果更佳
2、迭代器遍历,Iterator:
顺序存储:如果不是太在意时间,推荐选择此方式,毕竟代码更加简洁,也防止了Off-By-One的问题。
链式存储:意义就重大了,平均时间复杂度降为O(n),还是挺诱人的,所以推荐此种遍历方式。
3、foreach循环遍历:
foreach只是让代码更加简洁了,但是他有一些缺点,就是遍历过程中不能操作数据集合(删除等),所以有些场合不使用。而且它本身就是基于Iterator实现的,但是由于类型转换的问题,所以会比直接使用Iterator慢一点,但是还好,时间复杂度都是一样的。所以怎么选择,参考上面两种方式,做一个折中的选择。
Java数据集合框架中,提供了一个RandomAccess接口,该接口没有方法,只是一个标记。通常被List接口的实现使用,用来标记该List的实现是否支持Random Access。一个数据集合实现了该接口,就意味着它支持Random Access,按位置读取元素的平均时间复杂度为O(1)。比如ArrayList。而没有实现该接口的,就表示不支持Random Access。比如LinkedList。所以看来JDK开发者也是注意到这个问题的,那么推荐的做法就是,如果想要遍历一个List,那么先判断是否支持Random Access,也就是 list instanceof RandomAccess。
比如:
if (list instanceof RandomAccess) {
//使用传统的for循环遍历。
} else {
//使用Iterator或者foreach。
}
八、数据元素是怎样在内存中存放的?
数据元素在内存中,主要有2种存储方式:
1、顺序存储,Random Access(Direct Access):
这种方式,相邻的数据元素存放于相邻的内存地址中,整块内存地址是连续的。可以根据元素的位置直接计算出内存地址,直接进行读取。读取一个特定位置元素的平均时间复杂度为O(1)。正常来说,只有基于数组实现的集合,才有这种特性。Java中以ArrayList为代表。
2、链式存储,Sequential Access:
这种方式,每一个数据元素,在内存中都不要求处于相邻的位置,每个数据元素包含它下一个元素的内存地址。不可以根据元素的位置直接计算出内存地址,只能按顺序读取元素。读取一个特定位置元素的平均时间复杂度为O(n)。主要以链表为代表。Java中以LinkedList为代表。