Java基础-容器篇(概念性)

一、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.当进行增加或删除元素时,在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样,如果想删除一个元素,需要移动大量元素去填掉被移动的元素。而链表只需改动元素中的指针即可实现增加或删除元素。

综上,选择Hash可以具备数组的快速查询的优点又能融合链表方便快捷的增加删除元素的优势。但是,不相同的数据通过hash函数得到相同的key值。这时候,就产生了hash冲突。解决hash冲突的方式有两种。一种是挂链式,也叫拉链法。挂链式的思想在产生冲突的hash地址指向一个链表,将具有相同的key值的数据存放到链表中。另一种是建立一个公共溢出区。将所有产生冲突的数据都存放到公共溢出区,也可以使问题解决。

 

  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编程思想(第四版)》

 

posted @ 2020-11-19 19:02  浣花洗清秋  阅读(144)  评论(0编辑  收藏  举报