1. 程式人生 > >JAVA 基本數據結構--數組、鏈表、ArrayList、Linkedlist、hashmap、hashtab等

JAVA 基本數據結構--數組、鏈表、ArrayList、Linkedlist、hashmap、hashtab等

i++ 運算 prev ini 示意圖 insert builder double 上下

概要

線性表是一種線性結構,它是具有相同類型的n(n≥0)個數據元素組成的有限序列。本章先介紹線性表的幾個基本組成部分:數組、單向鏈表、雙向鏈表;隨後給出雙向鏈表的C、C++和Java三種語言的實現。內容包括:

出處:http://www.cnblogs.com/skywang12345/p/3561803.html

數組

數組有上界和下界,數組的元素在上下界內是連續的

存儲10,20,30,40,50的數組的示意圖如下:
技術分享圖片


數組的特點是:數據是連續的;隨機訪問速度快
數組中稍微復雜一點的是多維數組和動態數組。對於C語言而言,多維數組本質上也是通過一維數組實現的。至於動態數組,是指數組的容量能動態增長的數組;對於C語言而言,若要提供動態數組,需要手動實現;而對於C++而言,STL提供了Vector;對於Java而言,Collection集合中提供了ArrayList和Vector。

單向鏈表

單向鏈表(單鏈表)是鏈表的一種,它由節點組成,每個節點都包含下一個節點的指針。

單鏈表的示意圖如下:
技術分享圖片

表頭為空,表頭的後繼節點是"節點10"(數據為10的節點),"節點10"的後繼節點是"節點20"(數據為10的節點),...

單鏈表刪除節點

技術分享圖片

刪除"節點30"
刪除之前:"節點20" 的後繼節點為"節點30",而"節點30" 的後繼節點為"節點40"。
刪除之後:"節點20" 的後繼節點為"節點40"。

單鏈表添加節點

技術分享圖片

在"節點10"與"節點20"之間添加"節點15"
添加之前:"節點10" 的後繼節點為"節點20"。
添加之後:"節點10" 的後繼節點為"節點15",而"節點15" 的後繼節點為"節點20"。

單鏈表的特點是:節點的鏈接方向是單向的;相對於數組來說,單鏈表的的隨機訪問速度較慢,但是單鏈表刪除/添加數據的效率很高。

雙向鏈表

雙向鏈表(雙鏈表)是鏈表的一種。和單鏈表一樣,雙鏈表也是由節點組成,它的每個數據結點中都有兩個指針,分別指向直接後繼和直接前驅。所以,從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和後繼結點。一般我們都構造雙向循環鏈表

雙鏈表的示意圖如下:
技術分享圖片

表頭為空,表頭的後繼節點為"節點10"(數據為10的節點);"節點10"的後繼節點是"節點20"(數據為10的節點),"節點20"的前繼節點是"節點10";"節點20"的後繼節點是"節點30","節點30"的前繼節點是"節點20";...;末尾節點的後繼節點是表頭。

雙鏈表刪除節點

技術分享圖片

刪除"節點30"
刪除之前:"節點20"的後繼節點為"節點30","節點30" 的前繼節點為"節點20"。"節點30"的後繼節點為"節點40","節點40" 的前繼節點為"節點30"。
刪除之後:"節點20"的後繼節點為"節點40","節點40" 的前繼節點為"節點20"。

雙鏈表添加節點

技術分享圖片

在"節點10"與"節點20"之間添加"節點15"
添加之前:"節點10"的後繼節點為"節點20","節點20" 的前繼節點為"節點10"。
添加之後:"節點10"的後繼節點為"節點15","節點15" 的前繼節點為"節點10"。"節點15"的後繼節點為"節點20","節點20" 的前繼節點為"節點15"。

下面介紹雙鏈表的實現,介紹Java實現

雙鏈表類(DoubleLink.java)

技術分享圖片
/**
 * Java 實現的雙向鏈表。 
 * 註:java自帶的集合包中有實現雙向鏈表,路徑是:java.util.LinkedList
 *
 * @author skywang
 * @date 2013/11/07
 */
public class DoubleLink<T> {

    // 表頭
    private DNode<T> mHead;
    // 節點個數
    private int mCount;

    // 雙向鏈表“節點”對應的結構體
    private class DNode<T> {
        public DNode prev;
        public DNode next;
        public T value;

        public DNode(T value, DNode prev, DNode next) {
            this.value = value;
            this.prev = prev;
            this.next = next;
        }
    }

    // 構造函數
    public DoubleLink() {
        // 創建“表頭”。註意:表頭沒有存儲數據!
        mHead = new DNode<T>(null, null, null);
        mHead.prev = mHead.next = mHead;
        // 初始化“節點個數”為0
        mCount = 0;
    }

    // 返回節點數目
    public int size() {
        return mCount;
    }

    // 返回鏈表是否為空
    public boolean isEmpty() {
        return mCount==0;
    }

    // 獲取第index位置的節點
    private DNode<T> getNode(int index) {
        if (index<0 || index>=mCount)
            throw new IndexOutOfBoundsException();

        // 正向查找
        if (index <= mCount/2) {
            DNode<T> node = mHead.next;
            for (int i=0; i<index; i++)
                node = node.next;

            return node;
        }

        // 反向查找
        DNode<T> rnode = mHead.prev;
        int rindex = mCount - index -1;
        for (int j=0; j<rindex; j++)
            rnode = rnode.prev;

        return rnode;
    }

    // 獲取第index位置的節點的值
    public T get(int index) {
        return getNode(index).value;
    }

    // 獲取第1個節點的值
    public T getFirst() {
        return getNode(0).value;
    }

    // 獲取最後一個節點的值
    public T getLast() {
        return getNode(mCount-1).value;
    }

    // 將節點插入到第index位置之前
    public void insert(int index, T t) {
        if (index==0) {
            DNode<T> node = new DNode<T>(t, mHead, mHead.next);
            mHead.next.prev = node;
            mHead.next = node;
            mCount++;
            return ;
        }

        DNode<T> inode = getNode(index);
        DNode<T> tnode = new DNode<T>(t, inode.prev, inode);
        inode.prev.next = tnode;
        inode.next = tnode;
        mCount++;
        return ;
    }

    // 將節點插入第一個節點處。
    public void insertFirst(T t) {
        insert(0, t);
    }

    // 將節點追加到鏈表的末尾
    public void appendLast(T t) {
        DNode<T> node = new DNode<T>(t, mHead.prev, mHead);
        mHead.prev.next = node;
        mHead.prev = node;
        mCount++;
    }

    // 刪除index位置的節點
    public void del(int index) {
        DNode<T> inode = getNode(index);
        inode.prev.next = inode.next;
        inode.next.prev = inode.prev;
        inode = null;
        mCount--;
    }

    // 刪除第一個節點
    public void deleteFirst() {
        del(0);
    }

    // 刪除最後一個節點
    public void deleteLast() {
        del(mCount-1);
    }
}
View Code

ArrayList、Vector、LinkedList 區別與聯系:

看圖:

技術分享圖片

如上圖所示:

  ArrayList是實現了基於動態數組的數據結構,LinkedList基於雙線鏈表的數據結構。

  ArrayList可以隨機定位對於新增和刪除操作add和remove,LinedList比較占優勢

  具有Collection接口必備的iterator()方法外,List還提供一個listIterator()方法ListIterator多了一些add()之類的方法,允許添加,刪除,設定元素,還能向前或向後遍歷。

  Vector與ArrayList唯一的區別是,Vector是線程安全的,即它的大部分方法都包含有關鍵字synchronized,因此,若對於單一線程的應用來說,最好使用ArrayList代替Vector,因為這樣效率會快很多(類似的情況有StringBuffer線程安全的與StringBuilder線程不安全的);而在多線程程序中,為了保證數據的同步和一致性,可以使用Vector代替ArrayList實現同樣的功能。

主要區別:

1、ArrayList、Vector、LinkedList類都是java.util包中,均為可伸縮數組。

2、ArrayList和Vector底層都是數組實現的,所以,索引/查詢數據快,刪除、插入數據慢。

  ArrayList采用異步的方式,性能好,屬於非線程安全的操作類。(JDK1.2)

  Vector采用同步的方式,性能較低,屬於線程安全的操作類。(JDK1.0)

3、LinkedList底層是鏈表實現,所以,索引慢,刪除、插入快,屬於非線程安全的操作類

java定義數組需要聲明長度,然後arraylist基於數組,等同於一個動態數組的實現,但是查詢比較慢,所以可以自己編寫一個動態數組來實現,代碼如下:

技術分享圖片
package com.newer.tw.com;
 
 
/**
 * 自定義長度可變數組
 * 
 * @author Administrator
 * 
 */
public class MyList {
    // 定義一個初始長度為0的數組,用來緩存數據
    private String[] src = new String[0];
    // 增加
    public void add(String s) {
        //定義新數組,長度是原數組長度+1
        String[] dest = new String[src.length+1];
        //將原數組的數據拷貝到新數組
        System.arraycopy(src, 0, dest, 0, src.length);
        //將新元素放到dest數組的末尾
        dest[src.length]=s;
        //將src指向dest
        src=dest;
    }
    // 修改指定位置的元素
    public void modify(int index, String s) {
        src[index]=s;
    }
    // 刪除指定位置的元素
    public void delete(int index) {
        String[] dest = new String[src.length-1];
        //將原數組的數據拷貝到新數組
        System.arraycopy(src, 0, dest, 0, index);
        System.arraycopy(src, index+1, dest, index, src.length-1-index);
        src=dest;
    }
    // 獲得指定位置的元素
    public String get(int index) {
        return src[index];
    }
    // 在指定位置插入指定元素
    public void insert(int index, String s) {
        //定義新數組,長度是原數組長度+1
        String[] dest = new String[src.length+1];
        //將原數組的數據拷貝到新數組
        System.arraycopy(src, 0, dest, 0, index);
        dest[index]=s;
        System.arraycopy(src, index, dest, index+1, src.length-index);
        src=dest;
        
    }
    // 獲得元素個數
    public int size() {
        return src.length;
    }
    
    public void print()
    {
        for(int i=0;i<size();i++)
            System.out.println(src[i]);
    }
    
    public static void main(String[] args)
    {
        MyList m=new MyList();
        m.add("15");
        m.add("16");
        m.add("17");
        m.add("18");
        m.add("19");
        System.out.println("插入之前:");
        m.print();
        m.insert(2,"22");
        System.out.println("插入之後:");
        m.print();    
    }
 
}
View Code

Hashmap 原理

參考: https://blog.csdn.net/qa962839575/article/details/44889553

在java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用),所有的數據結構都可以用這兩個基本結構來構造的,hashmap也不例外。Hashmap實際上是一個數組和鏈表的結合體(在數據結構中,一般稱之為“鏈表散列“),請看下圖

技術分享圖片

從圖中我們可以看到一個hashmap就是一個數組結構,當新建一個hashmap的時候,就會初始化一個數組。

/** 
     * The table, resized as necessary. Length MUST Always be a power of two. 
     *  FIXME 這裏需要註意這句話,至於原因後面會講到 
     */  
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {  
        final K key;  
        V value;  
        final int hash;  
        Entry<K,V> next;  
..........  
}  

Entry就是數組中的元素,它持有一個指向下一個元素的引用,這就構成了鏈表。
當我們往hashmap中put元素的時候,先根據key的hash值得到這個元素在數組中的位置(即下標),然後就可以把這個元素放到對應的位置中了。如果這個元素所在的位子上已經存放有其他元素了,那麽在同一個位子上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。從hashmap中get元素時,首先計算key的hashcode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。從這裏我們可以想象得到,如果每個位置上的鏈表只有一個元素,那麽hashmap的get效率將是最高的,

static int indexFor(int h, int length) {  
       return h & (length-1);  
   }  

首先算得key得hashcode值,然後跟數組的長度-1做一次“與”運算(&)。看上去很簡單,其實比較有玄機。比如數組的長度是2的4次方,那麽hashcode就會和2的4次方-1做“與”運算。很多人都有這個疑問,為什麽hashmap的數組初始化大小都是2的次方大小時,hashmap的效率最高

當數組長度為2的n次冪的時候,不同的key算得得index相同的幾率較小,那麽數據在數組上分布就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
說到這裏,我們再回頭看一下hashmap中默認的數組大小是多少,查看源代碼可以得知是16,為什麽是16,而不是15,也不是20呢,看到上面annegu的解釋之後我們就清楚了吧,顯然是因為16是2的整數次冪的原因,在小數據量的情況下16比15和20更能減少key之間的碰撞,而加快查詢的效率。

初始容量為16,初始負載因子loadFactor為0.75 ,當hashmap中元素個數超過16*0.75=12的時候,就把數組的大小擴展為2*16=32,即擴大一倍;所以它帶有動態的意義

初始容量與負載因子

參考: https://www.cnblogs.com/haifeng1990/p/6262417.html

HashMap有兩個參數影響性能,初始容量和負載因子

JAVA 基本數據結構--數組、鏈表、ArrayList、Linkedlist、hashmap、hashtab等