一、Tree、Hash和Linked
Tree,即树,多数情况尤指二叉树,在C/C++中,树的实现依托于链表。二叉排序树是一种比较有用的折衷方案。数组的搜索比较方便,可以直接用下标,但删除或者插入某些元素就比较麻烦。链表与之相反,删除和插入元素很快,但查找很慢。二叉排序树就既有链表的好处,也有数组的好处。文件系统和数据库系统一般都采用树(特别是B树)的数据结构数据,主要为排序和检索的效率。
平衡二叉树都有哪些应用场景
二叉树支持动态的插入和查找,保证操作在O(height)时间,这就是完成了哈希表不便完成的工作,动态性。但是二叉树有可能出现worst-case,如果输入序列已经排序,则时间复杂度为O(N)
平衡二叉树/红黑树就是为了将查找的时间复杂度保证在O(logN)范围内。
所以如果输入结合确定,所需要的就是查询,则可以考虑使用哈希表,如果输入集合不确定,则考虑使用平衡二叉树/红黑树,保证达到最大效率
平衡二叉树主要优点集中在快速查找。
如果你知道SGI/STL的set/map底层都是用红黑树(平衡二叉树的一种)实现的,相信你会对这些树大有兴趣。
缺点:
顺序存储可能会浪费空间(在非完全二叉树的时候),但是读取某个指定的节点的时候效率比较高O(0)
链式存储相对二叉树比较大的时候浪费空间较少,但是读取某个指定节点的时候效率偏低O(nlogn)
Hash,哈希,即散列,即将输入的数据通过hash函数得到一个key值,输入的数据存储到数组中下标为key值的数组单元中去。
数组是将元素在内存中连续存放。
链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。
数组必须事先定义固定的长度,不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。
链表动态地进行存储分配,可以适应数据动态地增减的情况。
(静态)数组从栈中分配空间, 对于程序员方便快速,但是自由度小。
链表从堆中分配空间, 自由度大但是申请管理比较麻烦。
根据数组和链表的特性,数组和链表的优劣势分两类情况讨论。
a.当进行数据查询时,数组可以直接通过下标迅速访问数组中的元素。而链表则需要从第一个元素开始一直找到需要的元素位置,显然,数组的查询效率会比链表的高。
b.当进行增加或删除元素时,在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样,如果想删除一个元素,需要移动大量元素去填掉被移动的元素。而链表只需改动元素中的指针即可实现增加或删除元素。
Linked,链表,C中链表的单元节点为一个具有指针和数据的结构体,Java中为一个对象LinkNet。链表为一个个单元节点相连而成。长于增删,短于查找。
二、List、Set、Queue和Map
通常,程序总是根据运行时才知道的某些条件取创建新对象。在此之前,不会知道所需对象的数量,乃至其确切的类型。因为我们不知道要在何时何地创建何种数量的对象,所以我们就不能依靠创建命名的引用来持有每一个对象,因为引用的数量也是不确定的。
数组是一种一组对象或者最基本数据类型最有效的方式,然而数据的尺寸在声明时就已经固定了,当我们并不知道需要多少个对象时,或者需要更复杂的方式来存储对象时,数组尺寸固定就很不合适了。
Java实用类库提供了一套相当完整的容器来解决这类问题,其基本的类型是List、Set、Queue和Map。这类对象类型叫做集合类,但Java类库中已经使用了Collection来指代该类库中的一个特殊子集,所以将他们归类为表示范围更广的“容器”。
其中淡绿色的表示接口,红色的表示我们经常使用的类。
1、基本概念
根据Java容器类类库保存对象的用途来看,可以将其分为两类
Collection,一个独立元素的序列。List必须按照插入顺序保存元素,Set不能含有重复元素,Queue按照排队规则来确定对象产生的顺序。
Map,一组成对的“键值对”对象,允许用键来查找值。ArrayList从某种意义上是将数字与对象关联在一起。Map可以使用一个对象来查找某个对象,也称为映射表,关联数组,字典。
2、List
List承诺可以将元素维护在特定的序列中。List接口在Collection的基础上加入了大量的方法,使得可以在List中间可以插入和移除元素。
ArrayList,长于随机访问元素,但在List中插入和移除元素时较慢
LinkedList,长于插入和移除操作,有优化的顺序访问,但在随机访问方面比较慢。
关于ArrayList为什么在中间插入元素比较慢,代码如下
public void add(int index, E element) { rangeCheckForAdd(index);//验证(可以不考虑) ensureCapacityInternal(size + 1); // Increments modCount!!(超过当前数组长度进行扩容) System.arraycopy(elementData, index, elementData, index + 1, size - index);(核心代码) elementData[index] = element; size++; }
System.arraycopy(elementData, index, elementData, index + 1)第一个参数是源数组,源数组起始位置,目标数组,目标数组起始位置,复制数组元素数目。那么这个意思就是从index索性处每个元素向后移动一位,最后把索引为index空出来,并将element赋值给它。这样一来我们并不知道要插入哪个位置,所以会进行匹配那么它的时间赋值度就为n。
LinkedList采用的是链式存储。链式存储就会定一个节点Node。包括三部分前驱节点、后继节点以及data值。所以存储存储的时候他的物理地址不一定是连续的。
关于LinkedList的插入操作,代码如下
public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); }
void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
可以看出先获取插入索引元素的前驱节点,然后把这个元素作为后继节点,然后在创建新的节点,而新的节点前驱节点和获取前驱节点相同,而后继节点则等于要移动的这个元素。所以这里是不需要循环的,从而在插入和删除的时候效率比较高
3、Stack
“栈”是指后进先出(LIFO)的容器。LinkedList具有能够实现栈的所有功能的方法,因此可以直接将其作为栈使用。
4、Set
Set不保存重复的元素,这里的元素指的是某个对象的实例。Set最常被使用的是测试归属性,即查询某个对象是否在Set中。并且Set是具有和Collection完全一样的接口,没有额外的功能,只是表现的行为不同。HashSet这一实现对快速查找进行了优化。出于速度原因的考虑,HashSet使用了散列,但是其维护的顺序与TreeSet和LinkedHashSet都不同,这取决于他们的元素储存方式的实现不同,TreeSet将元素存储在红黑树数据结构中,而LinkedHashSet虽然也和HashSet一样使用散列,但其是使用链表来维护元素的插入顺序。所以如果想要对结果排序,使用TreeSet更合适。
5、Map
Map能将对象映射到其他对象,同时键具有不可重复性。Map在实际开发中使用非常广,特别是HashMap,想象一下我们要保存一个对象中某些元素的值,如果我们在创建一个对象显得有点麻烦,这个时候我们就可以用上map了,HashMap采用是散列函数所以查询的效率是比较高的,如果我们需要一个有序的我们就可以考虑使用TreeMap。这里主要介绍一下HashMap的方法,大家注意HashMap的键可以是null,而且键值不可以重复,如果重复了以后就会对第一个进行键值进行覆盖。
常用方法:put进行添加值键对,containsKey验证主要是否存在、containsValue验证值是否存在、keySet获取所有的键集合、values获取所有值集合、entrySet获取键值对。
6、Queue
Queue是队列,队列是典型的先进先出(FIFO)的容器,就是从容器的一端放入元素,从另一端取出,并且元素放入容器的顺序和取出的顺序是相同的。LinkedList提供了对Queue的实现,LinkedList向上转型为Queue。其中Queue有offer、peek、element、pool、remove等方法
offer是将元素插入队尾,返回false表示添加失败。peek和element都将在不移除的情况下返回对头,但是peek在对头为null的时候返回null,而element会抛出NoSuchElementException异常。poll和remove方法将移除并返回对头,但是poll在队列为null,而remove会抛出NoSuchElementException异常,以下是例子
public static void main(String[] args){ Queue<Integer> queue=new LinkedList<Integer>(); Random rand=new Random(); for (int i=0;i<10;i++){ queue.offer(rand.nextInt(i+10)); } printQ(queue); Queue<Character> qc=new LinkedList<Character>(); for (char c:"HelloWorld".toCharArray()){ qc.offer(c); } System.out.println(qc.peek()); printQ(qc); List<String> mystrings=new LinkedList<String>(); mystrings.add("1"); mystrings.get(0); Set<String> a=new HashSet<String>(); Set<String> set=new HashSet<String>(); set.add("1"); } public static void printQ(Queue queue){ while (queue.peek
三、Collections和Arrays
为了方便对Array对象、Collection对象进行操作,Java中提供了Arrays类和Collections类对其进行操作。
Arrsys:是数组的工具类,提供了对数组操作的工具方法。
Collections:是集合对象的工具类,提供了操作集合的工具方法。
其中Arrays和Collections中所有的方法都为静态的,不需要创建对象,直接使用类名调用即可。
1.Collections
Collection和Collections
- Collection:java.util.Collection 是描述所有序列容器的共性的根接口,是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。这与C++不同,标准C++类库中没有其容器的任何公共基类,容器之间的共性依靠迭代器达成。但在Java中,迭代器和Collection被绑定在了一起,实现Collection就意味着要提供iterator()方法。
- Collections:Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
关于Collections线程安全问题,先挖个坑,单开一贴。
Collection和Iterable
在jdk1.5中,Collection增加了一个父接口Iterable ,该接口的出现封装了iterator方法,并提供了一个增强型的for循环。
2。Arrays
Arrays提供了asList()和toArray()两个方法实现数组与集合的转化。
数组变集合
//数组中的元素都是对象 class ArraysDemo { public static void main(String[] args) { String[] arr={"abc","cc","kkk"}; list<String> list =Arrays.asList(arr); System.out.println(list) } } 运行结果: [abc,cc,kkk]
//数组中的元素都是基本数据类型 class ArraysDemo { public static void main(String[] args) { int[] num={2,3,4}; list<int[]> li =Arrays.asList(num); System.out.println(li) } } 运行结果:是一个数组的哈希值 [[I@de6ced]
如果数组中的元素都是对象,那么变成集合时,数组中的元素就直接转成集合中的元素
如果数组中的元素都是基本数据类型,那么会将该数组作为集合中的元素存在
这样可以使用集合的思想和方法来操作数组中的元素,但是,将数组变成集合,不可以使用集合的增删方法,因为数组的长度是固定的,如果你增删,那么会发生UnsupportedOperationException
集合变数组
当指定类型的数组长度小于了集合的size,那么该方法内部会创建一个新的数组,长度为集合的size
当指定类型的数组长度大于了集合的size,就不会新创建数组,而是使用传递进来的数组
所以创建一个刚刚好的数组最优,这是为了限定用户的操作,不需要其进行增删操作
public static void main(Stirng[] agrs) { ArrayList<String> list =new ArrayList<String>(); list.add("a1"); list.add("a2"); list.add("a3"); Stirng[] arr=list.toArray(new String[list.size()]); System.out.println(Arrays.toString(arr)); }
四、散列与散列码
我们知道Map以键值对的形式来存储数据。有一点值得说明的是,如果要使用我们自己的类作为键,我们必须同时重写hashCode() 和 equals()两个方法。HashMap使用equals方法来判断当前的键是否与表中的键相同。equals()方法需要满足以下5个条件
- 自反性 x.equals(x) 一定返回true
- 对称性 x.equals(y)返回true,则y.equals(x) 也返回true
- 传递性 x.equals(y)返回true,y.equals(z)返回true,则x.equals(y)返回true
- 一致性 如果对象中的信息没有改变,x.equals(y)要么一直返回true,要么一直返回false
- 对任何不是null的x,想x.equals(null)一定返回false
1.hashCode()
散列的价值在于速度:散列使得查询得以快速执行。由于速度的瓶颈是对“键”进行查询,而存储一组元素最快的数据结构是数组,所以用它来代表键的信息,注意:数组并不保存“键”的本身。而通过“键”对象生成一个数字,将其作为数组的下标索引。这个数字就是散列码,由定义在Object的hashCode()生成(或成为散列函数)。同时,为了解决数组容量被固定的问题,不同的“键”可以产生相同的下标。那对于数组来说?怎么在同一个下标索引保存多个值呢??原来数组并不直接保存“值”,而是保存“值”的 List。然后对 List中的“值”使用equals()方法进行线性的查询。这部分的查询自然会比较慢,但是如果有好的散列函数,每个下标索引只保存少量的值,只对很少的元素进行比较,就会快的多。
参考资料:《Java编程思想(第四版)》