没有最好的,只有最合适的。


目录

  • 摘要
  • 一、数组
  • 1.1 数组的缺点
  • 二、链表
  • 2.1 链表中间元素的删除原理
  • 2.2 链表的简单使用
  • 2.3 链表的缺点
  • 三、散列表
  • 3.1 散列表结构
  • 3.2 散列表原理简述
  • 四、树集
  • 4.1 树集简述
  • 4.2 树集的简单使用:
  • 五、映射
  • 5.1 关于映射
  • 5.2 映射的简单使用


摘要

在你有一定技术能力上,如果哪天处理数据你发现你正在使用的方法对于要解决的问题效率不高,可能是因为你使用了错误的数据结构。

  • 数组/数组列表:可以使用索引很方便的访问到指定位置的数据,但有一个重大的缺陷就是在数组中间添加或删除一个元素开销很大。
  • 链表:而对于链表而言能在很大程度减小这个开销,但是它失去了快速随机访问,哪怕使用get(index)方法,依旧是进行遍历访问。
  • 散列集:链表和数组允许我们指定元素的次序,但是当我们想查看某个元素却又不记得它的位置就需要访问所有元素直到找到为止,如果不在意元素的顺序,有几种可以能够快速查询元素的数据结构,缺点是无法控制元素出现的次序,例如散列表。
  • 树集:TreeSet类相比散列集有所改进,它是一个有序集合。可以以任意顺序插入元素,在对集合遍历时,值将自动地按照排序后的顺序呈现。而且,与检查数组或链表中的重复元素相比,使用树会快很多。当然,将一个元素添加到树中要比添加到散列表中慢,而且要使用树集,必须能够比较元素。
  • 映射:上面所说的,集是一个集合,允许快速查找现有的元素,但是通常情况下我们会知道某些关键信息,希望去查找与之关联的元素。映射便是为此设计,它用来存放键值对,也是我们很熟悉的结构。

一、数组

1.1 数组的缺点

在我们程序日常应用中,有很多地方都用到了数组以及动态的ArrayList,但是他们有一个很大的缺陷,就是从数组中间删除一个元素需要付出很大的代价,原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动, 在数组中间的位置上插入一个元素也是一样的道理。

java数据结构小技巧 java 中的数据结构_链表


二、链表

2.1 链表中间元素的删除原理

而另外一个数据结构——链表就可以解决这个问题。数组是在连续的存储位置上存放对象引用,而链表则是将每个对象存放在单独的链接中。在Java中,所有的链表都是双向链接,每个链接中还存放着指向前驱的引用。

从链表中删除一个元素很简单,只需要将这个元素相邻的两个元素链接起来,将要删除的元素从链接中剔除即可。例如链表中分别有A、B、C三个链接,如果要删除B,只需要将A指向B的引用替换成A指向C的引用;将C指向B的引用替换成C指向A的引用。插入也是相同的道理。

java数据结构小技巧 java 中的数据结构_数组_02



2.2 链表的简单使用

链表是一个有序集合,每个对象的位置十分重要。LinkedList类中的add方法可以将对象添加到链表的尾部。但是我们之所以使用链表的一大原因就是需要将元素添加到链表的中间,而这种依赖于位置的add方法将有迭代器负责,下面是链表具体使用的两个简单小例子。

链表的简单使用1:先添加3个元素,再删除第二个元素

public static void main(String[] args) {
    LinkedList<String> students = new LinkedList<String>();
    students.add("Xiao Ming");
    students.add("Xiao Hong");
    students.add("Xiao Hua");
    Iterator<String> iter = students.iterator();
    String first = iter.next();
    System.out.println("first:" + first);	//first:Xiao Ming
    String second = iter.next();
    System.out.println("second:" + second);	//second:Xiao Hong
    iter.remove();
    System.out.println("Now students have:" + students);	//Now students have:[Xiao Ming, Xiao Hua]
}

链表的简单使用2:先添加3个元素,在第一个与第二个元素之中添加一个元素

public static void main(String[] args) {
    LinkedList<String> students = new LinkedList<String>();
    students.add("Xiao Ming");
    students.add("Xiao Hong");
    students.add("Xiao Hua");
    ListIterator<String> iter = students.listIterator();
    iter.next();
    iter.add("New Student");
    System.out.println("Now students have:" + students);	//Now students have:[Xiao Ming, New Student, Xiao Hong, Xiao Hua]
}

要注意以上两个代码快中的迭代器是不一样的哦!

2.3 链表的缺点

链表的一个缺点是不支持快速随机访问,它不像数组那样,可以通过索引直接访问。要查看链表中的第n个元素,就必须从头开始,越过n-1个元素。虽然LinkedList类还是提供了一个用来访问某个特定元素的get方法,但是这个方法的本质6依旧是从头开始遍历。尽管现在的get(n)方法做了一个微小的优化——如果n > size()/2 ,就从列表尾端开始搜索元素,但get方法依旧效率不太高。
很明显我们使用链表的唯一理由是尽可能地减少在列表中间插入或删除元素的开销。



三、散列表

3.1 散列表结构

链表和数组可以按照我们的意愿排列元素,当我们想要查看某个指定的元素,却忘记了它的位置,就只能通过遍历所有元素直到找到该元素为止。可想而知,当集合中包含很多元素,这样查找将会耗费很多时间。
如果我们不需要在意集合中元素的顺序,有几种能够快速查找元素的数据结构,例如散列表。散列表会为每一个对象计算一个整数,这个整数称为散列码(hash code),是的,hash code,正如你所想的那样,这个散列码和hashCode方法联系紧密。String类的对象的散列码就是有String类中的hashCode方法产生的。而如果我们需要自定义类,就要负责实现这个类的hashCode方法,这里要注意,自己实现的hashCode方法要与equals方法兼容!
这些不是本文重点不再赘述。

3.2 散列表原理简述

在Java中,散列表用链表数组实现。每个列表被称为桶(bucket),要想查找表中对象的位置,就要先计算出它的散列码,然后与桶的总数取余,余数就是保存这个元素的桶的索引。
在元素插入桶中时,有时候会遇到桶已经被填充的情况。这种现象被称为散列冲突。这时需要先将新对象与桶中已经存在的对象进行比较,查看这个对象是否已经存在。如果散列码合理地随机分布,桶的数目也足够大,需要比较的次数会比较少。

散列表是根据关键码值(Key-value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

看起来十分复杂,但其实就是通过键值对的方式存储和查找。而这种操作,与我们经常会用到的HashMap非常相似,事实上,HashMap内部就是散列表数据结构,因此我们完全可以通过常用的HashMap来帮助我们理解散列表。可以看1.8官方文档中关于HashMap的介绍:

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets. Iteration over collection views requires time proportional to the “capacity” of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings). Thus, it’s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.

HashMap基于哈希表的实现的Map接口。该实现提供了所有可选的映射操作,并允许null值和null键。( HashMap类大致相当于Hashtable ,除了它是异步的,并允许null)。这个类不能保证映射的顺序,特别是,它不能保证顺序在一段时间内保持不变。 原因是,如果元素的散列码发生了改变,元素在数据结构中的位置也会发生变化,因此在更改散列表中的元素是要格外小心!

四、树集

4.1 树集简述

TreeSet相对散列集有所改进,它是一个有序集合,可以以任意顺序将元素插入到集合中,在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现(排序使用树结构完成的,当前实现使用的是红黑树)。每次将一个元素添加到树中时,都将放在正确的排序位置上,因此将一个元素添加到树中要比添加到散列表中慢,但是与添加到数组或链表的正确位置上相比还是快很多的。

4.2 树集的简单使用:

//Class Test
public static void main(String[] args) {

    TreeSet<Student> students = new TreeSet<Student>();
    students.add(new Student("Xiao Ming",19));
    students.add(new Student("Xiao Hong",20));
    students.add(new Student("Xiao Hua",18));
    System.out.println(students);
}

//Class Student
public class Student {
    private String name;
    private Integer age;
    public Student (String name,Integer age){
        this.name = name;
        this.age = age;
    }
}

执行结果:会抛出一个 异常:java.lang.ClassCastException

Exception in thread "main" java.lang.ClassCastException: com.macro.mall.demo.dto.Student cannot be cast to java.lang.Comparable
	at java.util.TreeMap.compare(TreeMap.java:1294)
	at java.util.TreeMap.put(TreeMap.java:538)
	at java.util.TreeSet.add(TreeSet.java:255)
	at com.macro.mall.demo.controller.Test.main(Test.java:40)

原因在于我们需要告诉TreeSet如何来进行比较元素,如果不指定,就会抛出这个异常。首先,我们需要在自定义类(Student)中实现Comparable接口,并重写接口中的compareTo方法,这里我使用了最简单最简单的例子,应该没有更简单的例子了:

public class Student implements Comparable<Student> {
    private String name;
    private Integer age;
    public Student (String name,Integer age){
        this.name = name;
        this.age = age;
    }
    @Override
    public int compareTo(Student o) {
        int result = Integer.compare(age,o.age);
        return result;
    }
}

输出结果:[Student(name=Xiao Hua, age=18), Student(name=Xiao Ming, age=19), Student(name=Xiao Hong, age=20)]

五、映射

5.1 关于映射

集是一个集合,它可以快速地查找现有的元素,但是在集合中查找元素通常要进行遍历查询。而我们有时候知道某些键的信息,并且想通过键去查找到对应的元素,映射表就是为了解决这个问题而诞生的。映射存放的是我们非常熟悉的键值对,值可以重复,键是唯一的。

Java类库提供了两个通用的实现,也是我们最常用到的两个类:HashMap和TreeMap,这两个类都实现了Map接口。散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索树。与集一样,散列映射比树映射要快,因此如果不需要按照有序的顺序访问键,最好选择散列映射。

有趣的是,集合框架本身不认为映射是一个集合,而其他数据结构框架认为映射是一个键值对集合,或者说是按键索引的值集合。通常,我们会通过得到映射的视图来得到集合,有三种视图:键集、值集合、键值对集。对应的方法为:

  • Set KeySet()
  • Collection values()
  • Set<Map.Entry<K, V>> entrySet()

5.2 映射的简单使用

下面举一个小例子来看一下映射的使用,依旧是简单至上:

public static void main(String[] args) {
    HashMap<Integer,Student> students = new HashMap<Integer,Student>();
    students.put(1,new Student("Xiao Ming",18));
    students.put(2,new Student("Xiao Hong",18));
    students.put(3,new Student("Xiao Hua",18));
    students.forEach((k,v) ->
    System.out.println( "id:" + k +"  name:"+ v.getName() + "  age:" + v.getAge() ) );
}
//	输出结果:
//	id:1  name:Xiao Ming  age:18
//	id:2  name:Xiao Hong  age:18
//	id:3  name:Xiao Hua  age:18

关于Java中的几个常见数据结构就简单介绍到这,重点是对比不同的数据结构的优缺点,在合适的环境使用合适的数据结构。大家可以自行去深入研究每一个数据结构,Fighting!