1. 程式人生 > >玩轉資料結構——第六章:集合和對映

玩轉資料結構——第六章:集合和對映

集合(Set)

什麼是集合?

集合是承載元素的容器;

特點:每個元素只能存在一次

優點:去重

  • 二分搜尋樹的新增操作add:不能盛放重複元素
  • 是非常好的實現“集合”的底層資料結構
/**
 * 集合的介面
 */
public interface Set<E> {
    void add(E e);//新增  <——<不能新增重複元素
    void remove(E e);//移除
    int  getSize();//獲取大小
    boolean isEmpty();//是否為空
    boolean contains(E e);//是否包含元素
    
}

典型應用:1.客戶統計 2.詞彙量統計

1-1基於二分搜尋樹實現集合Set

/**
 * 基於BST二分搜尋樹實現的集合Set
 *
 * @param <E>
 */

public class BSTSet<E extends Comparable<E>> implements Set<E> {//元素E必須滿足可比較的

    private BST<E> bst;//基於BST類的物件

    //建構函式
    public BSTSet() {
        bst = new BST<>();
    }

    //返回集合大小
    @Override
    public int getSize() {
        return bst.size();
    }

    //返回集合是否為空
    @Override
    public boolean isEmpty() {
        return bst.isEmpty();
    }

    //Set新增元素
    @Override
    public void add(E e) {
        bst.add(e);
    }

    //是否包含元素
    @Override
    public boolean contains(E e) {
        return bst.contain(e);
    }

    //移除元素
    @Override
    public void remove(E e) {
        bst.remove(e);
    }
}

測試:兩本名著的詞彙量 和不重複的詞彙量

    public static void main(String[] args) {

        System.out.println("Pride and Prejudice");
        //新建一個ArrayList存放單詞
        ArrayList<String> words1=new ArrayList<>();
        //通過這個方法將書中所以單詞存入word1中
        FileOperation.readFile("pride-and-prejudice.txt",words1);
        System.out.println("Total words : "+words1.size());

        BSTSet<String> set1=new BSTSet<>();
        //增強for迴圈,定一個字串word去遍歷words
        //底層的話會把ArrayList words1中的值一個一個的賦值給word
        for(String word:words1)
            set1.add(word);//不新增重複元素
        System.out.println("Total  different words : "+set1.getSize());


        System.out.println("-------------------");
        System.out.println("Pride and Prejudice");
        //新建一個ArrayList存放單詞
        ArrayList<String> words2=new ArrayList<>();
        //通過這個方法將書中所以單詞存入word1中
        FileOperation.readFile("a-tale-of-two-cities.txt",words2);
        System.out.println("Total words : "+words2.size());

        BSTSet<String> set2=new BSTSet<>();
        //增強for迴圈,定一個字串word去遍歷words
        //底層的話會把ArrayList words1中的值一個一個的賦值給word
        for(String word:words2)
            set2.add(word);//不新增重複元素
        System.out.println("Total  different words : "+set2.getSize());

    }

結果:

Pride and Prejudice
Total words : 125901
Total  different words : 6530
-------------------
Pride and Prejudice
Total words : 141489
Total  different words : 9944

1-2基於連結串列(LinkedList)的實現集合實現

  • BST和LinkedList都屬於動態資料結構

程式碼: 

public class LinkedListSet<E> implements Set<E> {
    /**
     * 基於連結串列實現集合類
     */

    private LinkedList<E> list;//建立一個連結串列物件

    //建構函式
    public LinkedListSet() {
        list = new LinkedList<>();//初始化
    }

    //重寫集合的方法
    @Override
    public int getSize() {
        return list.getSize();
    }

    //集合是否為空
    public boolean isEmpty() {
        return list.isEmpty();
    }

    //是否包含元素e
    public boolean contains(E e) {
        return list.contains(e);
    }

    /**
     * 新增元素
     * Set中不能新增重複元素
     * LinkedList可以新增重複元素
     * 解決方式:先判斷是否包含此元素
     * 不包含再新增
     */
    @Override
    public void add(E e) {
        if (!list.contains(e)) {
            list.addFirst(e);
        }

    }

    @Override
    public void remove (E e){
        list.removeElement(e);
    }


}

測試:和上面用二分搜尋的測試結果一樣,不過時間比它的長(測試用例:將上面BSTSet改成LinkedListSet即可)

1-3.兩種集合類的複雜度分析

測試兩種集合類查詢單詞所用的時間

public class Main {
    //建立一個測試方法 Set<String> set:他們可以是實現了該介面的LinkedListSet和BSTSet物件
    private static double testSet(Set<String> set, String filename) {
        //計算開始時間
        long startTime = System.nanoTime();
        System.out.println("Pride and Prejudice");
        //新建一個ArrayList存放單詞
        ArrayList<String> words1 = new ArrayList<>();
        //通過這個方法將書中所以單詞存入word1中
        FileOperation.readFile(filename, words1);
        System.out.println("Total words : " + words1.size());

        //增強for迴圈,定一個字串word去遍歷words
        //底層的話會把ArrayList words1中的值一個一個的賦值給word
        for (String word : words1)
            set.add(word);//不新增重複元素
        System.out.println("Total  different words : " + set.getSize());

        //計算結束時間
        long endTime = System.nanoTime();
        return (endTime - startTime) / 1000000000.0;//納秒為單位
    }

    public static void main(String[] args) {
        //基於二分搜尋的集合
        BSTSet<String> bstSet = new BSTSet<>();
        double time1 = testSet(bstSet, "pride-and-prejudice.txt");
        System.out.println("BSTSet:" + time1 + "s");
        System.out.println("————————————————————");
        //基於連結串列實現的集合
        LinkedListSet<String> linkedListSet = new LinkedListSet<>();
        double time2 = testSet(linkedListSet, "pride-and-prejudice.txt");
        System.out.println("linkedListSet:" + time2 + "s");

    }
}

結果:BSTSet的速度比LinkedListed的速度快 

Pride and Prejudice
Total words : 125901
Total  different words : 6530
BSTSet:0.263255132s
————————————————————
Pride and Prejudice
Total words : 125901
Total  different words : 6530
linkedListSet:4.080751976s

集合的時間複雜度分析:

集合操作 LinkedListSet BSTSet
增add O(n),增之前查詢一遍

O(h)=O(logn)

(平均)是O(logn)

(最差)O(n):

二分搜尋樹可能退化成連結串列

h:

二分搜尋樹的深度

每一次操作都是一層

一層的往下找

查contains O(n)
刪remove O(n),刪也要查詢

logn和n的差距

logn n logn和n的差距
n=16 4 16 相差4倍
n=1024 10 1024 相差100倍
n=100萬 20 100萬 相差5萬倍

二分搜尋樹:

滿二叉樹的情況(每個節點都有左右節點,除了葉子節點)

層數 這層的節點數 (0-h-1)h層,一共有多少個節點
0 1個節點 2^0+2^1+2^2+2^3+2^4...+2^(h-1=2^{h}-1
1 2個節點 等比數列: a1*(1-q^n)/(a1-q1)
2 4個節點 h=log_{2}(n+1)=O(log{_{2}}n)
=O(log{n)
3 8個節點
4 16個節點 底數是多少不影響它是log級別的
h-1 2^(h-1)個節點 相當於O(1/2n)=O(n) 

7-4.LeetCode中的集合問題和更多集合相關的問題

804.唯一的摩斯密碼詞

國際摩爾斯密碼定義一種標準編碼方式,將每個字母對應於一個由一系列點和短線組成的字串, 比如: "a" 對應 ".-""b" 對應 "-...""c" 對應 "-.-.", 等等。

為了方便,所有26個英文字母對應摩爾斯密碼錶如下:

[".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."]

給定一個單詞列表,每個單詞可以寫成每個字母對應摩爾斯密碼的組合。例如,"cab" 可以寫成 "-.-.-....-",(即 "-.-." + "-..." + ".-"字串的結合)。我們將這樣一個連線過程稱作單詞翻譯。

返回我們可以獲得所有詞不同單詞翻譯的數量。

例如:
輸入: words = ["gin", "zen", "gig", "msg"]
輸出: 2
解釋: 
各單詞翻譯如下:
"gin" -> "--...-."
"zen" -> "--...-."
"gig" -> "--...--."
"msg" -> "--...--."

共有 2 種不同翻譯, "--...-." 和 "--...--.".
注意:
  • 單詞列表words 的長度不會超過 100
  • 每個單詞 words[i]的長度範圍為 [1, 12]
  • 每個單詞 words[i]只包含小寫字母。

思路:

將摩斯密碼轉換表用字串陣列存起來

然後遍歷輸入的字串組,拿到當前字串(chatAt())的索引(根據索引才能利用轉化表)將其轉換為摩斯密碼並拼接到res中,

並用一個集合資料結構(TreeSet)來儲存add(res.toString())進去實現去重功能

最後返回這個集合的元素個數即可

public class Solution {
    public static int uniqueMorseRepresentations(String[] words) {
        String[] codes = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..",
                ".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...",
                "-", "..-", "...-", ".--", "-..-", "-.--", "--.."};

        /**
         * TreeSet基於紅黑樹實現的集合
         */
        //新建TreeSet一個物件
        TreeSet<String> set = new TreeSet<>();
        //遍歷word陣列
        for (String word : words) {
            StringBuilder res = new StringBuilder();
            for (int i = 0; i < word.length(); i++) {
                word.charAt(i);//獲得當前字元
                //獲得在code是的索引,將轉換的Morse碼拼接到res中
                res.append(codes[word.charAt(i) - 'a']);//a-a=0,b-a=1
            }
            set.add(res.toString());//將res裡面的字串新增到set中,set會自動幫我們去重
            System.out.println(res.toString());
        }
        return set.size();
    }

測試:

 public static void main(String[] args) {
        String[] words = {"gin", "zen", "gig", "msg"};
        System.out.println("字串中的莫斯密碼有:" + uniqueMorseRepresentations(words) + "種");
    }

結果:

--...-.
--...-.
--...--.
--...--.
字串中的莫斯密碼有:2兩種

有序集合和無序集合

有序集合:元素中具有順序性<——<基於搜尋樹實現

無序集合:上一節連結串列實現的集合是無序的集合,元素沒有順序性<——<基於雜湊表的實現

多重集合

  • 集合中的元素可以重複

7.5對映Map(字典)

  • 一一對映,在定義域中每一個值在值域都有一個值與他對應
  • 儲存(鍵,值)資料對的資料結構(Key,Value)
  • 根據鍵(Key),尋找值(Value)

定義Map的介面

public interface Map<K, V> {
    //增
    void add(K key, V value);
    //刪
    V remove(K key);
    //查
    boolean contains(K key);
    V get(K key);
    //改
    void set(K key, V value);
    //獲取字典大小
    int getize();
    boolean isEmpty();

}

7.6基於連結串列實現對映類Map

整體結構:

獲取對映大小和判空操作

    @Override
    public int getSize() {
        return size;
    }
    @Override
    public boolean isEmpty() {
        return size == 0;
    }

輔助方法:通過key值檢視是否包含這個節點,如果有返回這個節點 

 /**
     * 輔助方法
     * 通過key值返回這個節點
     *
     * @param key
     * @return
     */
    private Node getNode(K key) {
        Node cur = dummyHead.next;
        while (cur != null) {
            if (cur.key.equals(key)) {
                return cur;//返回找到這個節點
            }
            cur = cur.next;
        }
        return null;//找不到這個節點返回null
    }

查詢操作

 //看是否有這個資料
    @Override
    public boolean contains(K key) {
        return getNode(key) != null;
    }
    //操作操作
    @Override
    public V get(K key) {
        Node node = getNode(key);//找到這個節點
        return node == null ? null : node.value;//如果這個節點為空則返回空,如果不為空返回這個元素
    }

 新增操作

    /**
     * 新增操作
     *
     * @param key//key不能重複
     * @param value
     */
    @Override
    public void add(K key, V value) {
        Node node = getNode(key);//確認是否已經有這個資料
        if (node == null) {
            //它的next就等於當前dummyHead.next
            dummyHead.next = new Node(key, value, dummyHead.next);
            size++;
        } else {//也可以做提醒新增重複的值,或者更新它的value
            node.value = value;//更新
        }
    }

更新操作

    /**
     * 更新操作
     */
    @Override
    public void set(K key, V newValue) {
        Node node = getNode(key);//看是否有這個節點
        if (node == null) {
            throw new IllegalArgumentException(key + "does't exist!");
        }
        node.value = newValue;
    }

 刪除操作

/**
     * 刪除操作
     */
    @Override
    public V remove(K key) {
        Node prev = dummyHead;
        while (prev.next != null) {
            if (prev.next.key.equals(key)) {
                Node delNode = prev.next;
                prev.next = delNode.next;
                delNode.next = null;
                size--;
                return delNode.value;//返回這個刪除元素的
            }
            prev = prev.next;
        }
        return null;//如果沒有這個key
    }

測試:

    /**
     * 測試
     */
    public static void main(String[] args) {
        System.out.println("Pride and Prejudice");
        ArrayList<String> words = new ArrayList<>();
        if (FileOperation.readFile("pride-and-prejudice.txt", words)) {
            System.out.println("Total words:" + words.size());
            //key,value找出每個單詞出現的頻率
            LinkedListMap<String, Integer> map = new LinkedListMap<>();
            for (String word : words) {
                if (map.contains(word)) {
                    //如果有這個單詞,則通過get(key)找到這個value值並加1
                    map.set(word, map.get(word) + 1);
                } else
                    //如果單詞是第一次出現
                    map.add(word, 1);//頻率初始值是1
            }
            System.out.println("Toal different words:" + map.getSize());
            //檢視pride單詞出現的頻率
            System.out.println("Frequency of PRIDE:" + map.get("pride"));
            System.out.println("Frequency of PREJUDICE:" + map.get("prejudice"));
        }
    }

結果:

Pride and Prejudice
Total words:125901
Toal different words:6530
Frequency of PRIDE:53
Frequency of PREJUDICE:11

7.7基於二分搜尋樹實現對映類Map

定義節點內部類

   //定義節點內部類
    private class Node {
        public K key;//鍵
        public V value;//值
        public Node left, right;//節點next

        //節點的構造方法
        public Node(K key, V value) {
            this.key = key;//使用者傳進來的key賦值給當前節點的key
            this.value = value;//使用者傳來的value當前節點的value
            right = null;
            left = null;
        }
    } 

簡單的初始化和獲取map大小和判空的方法

   private Node root;//根節點
    private int size;
    //建構函式
    public BSTMap() {
        root = null;
        size = 0;
    }
    @Override
    public int getSize() {
        return size;
    }
    @Override
    public boolean isEmpty() {
        return size == 0;
    }

新增操作:

    //向二分搜尋樹中新增新的元素(Key,Value)
    @Override
    public void add(K key, V value) {
        root = add(root, key, value);
    }
    /**
     * 二分搜尋樹:當前節點
     * 大於其左子樹的所有節點的值
     * 小於其右子樹的所有節點的值
     *
     * @param node
     * @param key
     * @return
     */
    //向以node為根節點的二分搜尋樹插入元素(key,value),遞迴演算法,//返回插入新節點後二分搜尋樹的根
    private Node add(Node node, K key, V value) {
        //1最基本問題的解決
        if (node == null) {//還有一種是從空節點的二叉樹中插入元素
            size++;//記錄元素個數
            return new Node(key, value);
        }
        if (key.compareTo(node.key) < 0) {//當前指定的元素e小於node的值,則在左邊插入
            node.left = add(node.left, key, value);//呼叫遞迴
        } else if (key.compareTo(node.key) > 0) {//當前指定的元素e大於node的值,則在右邊插入
            node.right = add(node.right, key, value);
        } else//key.compareTo(node.key)==0
            node.value = value;
        return node;//返回插入新節點後二分搜尋樹的根
    }

輔助函式

//返回以node為根節點的二分搜尋樹中,Key所在的節點
    private Node getNode(Node node, K key) {
        if (node == null) {
            return null;
        }
        if (key.compareTo(node.key) == 0) {//找到了
            return node;
        } else if (key.compareTo(node.key) < 0)
            //遞迴繼續去node的左子樹中尋找
            return getNode(node.left, key);
        else//key.compareTo(node.key)>0
            //遞迴繼續去node的左子樹中尋找
            return getNode(node.right, key);
    }

更新、一些其他方法

 @Override
    public boolean contains(K key) {
        return getNode(root, key) != null;//從根節點開始尋找這個key
    }
    //通過key返回value
    @Override
    public V get(K key) {
        Node node = getNode(root, key);
        return node == null ? null : node.value;
    }
    @Override
    public void set(K key, V newValue) {
        Node node = getNode(root, key);
        if (node == null)//如果沒有這個元素
            throw new IllegalArgumentException(key + "doesn't exist!");
        node.value = newValue;//更新value值
    }

刪除操作:難點

    // 返回以node為根的二分搜尋樹的最小值所在的節點
    private Node minimum(Node node) {
        if (node.left == null)
            return node;

        return minimum(node.left);
    }
    // 刪除掉以node為根的二分搜尋樹中的最小節點
    // 返回刪除節點後新的二分搜尋樹的根
    private Node removeMin(Node node) {

        if (node.left == null) {
            Node rightNode = node.right;
            node.right = null;
            size--;
            return rightNode;
        }

        node.left = removeMin(node.left);
        return node;
    }
    //從二分搜尋樹中刪除鍵為key的節點
    public V remove(K key) {
        Node node = getNode(root, key);
        if (node != null) {
            root = remove(root, key);
            return node.value;
        }
        return null;
    }
    //刪除以node為根的二分搜尋樹中鍵為key的節點,遞迴演算法
    //返回刪除節點後新的二分搜尋樹的根
    private Node remove(Node node, K key) {
        if (node == null)
            return null;
        if (key.compareTo(node.key) < 0) {//要刪除的元素e比當前元素小
            node.left = remove(node.left, key);
            return node;
        } else if (key.compareTo(node.key) > 0) {
            node.right = remove(node.right, key);
            return node;
        } else {//e.equals(node.e)
            //待刪除節點的左子樹為空的情況
            if (node.left == null) {
                Node rightNode = node.right;//儲存一下這個孩子的右子樹
                node.right = null;//右子樹
                size--;
                return rightNode;
            }
            //待刪除節點的右子樹為空的情況
            if (node.right == null) {
                Node leftNode = node.left;
                node.left = null;//讓當前的node與二分搜尋樹脫離 滿足node.left=node.right=null
                size--;
                return leftNode;
            }
            //待刪除的節點左右子樹均不為空
            //找到比待刪除節點到達的最小節點,即待刪除節點的右節點的最小節點
            //用這個節點頂替待刪除節點的位置
            Node successor = minimum(node.right);//找到當前節點右子樹最小的值
            //successor為頂替待刪除的節點(後繼)
            successor.right = removeMin(node.right);//將node.right的最小值給刪除
            successor.left = node.left;

            node.left = node.right = null;//讓當前node與二分搜尋樹脫離關係
            return successor;

        }
    }

測試結果:和用LinkedList實現的結果一樣,但速度快多了

7.8對映的複雜度分析和更多對映相關的問題

基於二分搜尋樹和基於連結串列實現的對映的時間差異:

public class MainMap {
    //建立一個測試方法 Set<String> set:他們可以是實現了該介面的LinkedListSet和BSTSet物件
    private static double testMap(Map<String, Integer> map, String filename) {
        //計算開始時間
        long startTime = System.nanoTime();
        System.out.println("Pride and Prejudice");
        ArrayList<String> words = new ArrayList<>();
        if (FileOperation.readFile(filename, words)) {
            System.out.println("Total words:" + words.size());
            //key,value找出每個單詞出現的頻率
            for (String word : words) {
                if (map.contains(word)) {
                    //如果有這個單詞,則通過get(key)找到這個value值並加1
                    map.set(word, map.get(word) + 1);
                } else
                    //如果單詞是第一次出現
                    map.add(word, 1);//頻率初始值是1
            }
            System.out.println("Toal different words:" + map.getSize());
            //檢視pride單詞出現的頻率
            System.out.println("Frequency of PRIDE:" + map.get("pride"));
            System.out.println("Frequency of PREJUDICE:" + map.get("prejudice"));
        }
        //計算結束時間
        long endTime = System.nanoTime();
        return (endTime - startTime) / 1000000000.0;//納秒為單位
    }

    public static void main(String[] args) {
        String filename = "pride-and-prejudice.txt";
        BSTMap<String, Integer> bstMap = new BSTMap<>();
        double time1 = testMap(bstMap, filename);
        System.out.println("BST Map: " + time1 + " s");
        System.out.println("------------");
        LinkedListMap<String, Integer> linkedListMap = new LinkedListMap<>();
        double time2 = testMap(linkedListMap, filename);
        System.out.println("LinkedList Map: " + time2 + " s");
    }
}

結果:

Pride and Prejudice
Total words:125901
Toal different words:6530
Frequency of PRIDE:53
Frequency of PREJUDICE:11
BST Map: 0.358648249 s
------------
Pride and Prejudice
Total words:125901
Toal different words:6530
Frequency of PRIDE:53
Frequency of PREJUDICE:11
LinkedList Map: 23.002140494 s

時間複雜度分析:

有序對映和無序對映

有序對映中鍵具有順序性的<——<基於搜尋樹實現

無序對映中鍵沒有順序性的 <——<基於雜湊表實現

多重對映:鍵可以重複的

集合對映的關係

  • 基於集合(Set<E>)的實現來實現對映(Map<K,V>)

重定義集合中的元素是<K,V>

重定義的資料對是以K鍵來進行比較的,而不去管value值

  • 基於對映(Map<K,V>)實現來實現集合(Set<E>)更常見的方法

 當我們有一個底層對映實現了

集合我們就可以理解成Map<K,V>中的V值為null的情況

對不管是什麼K,它所對應的V都是空

當我們只考慮K的時候,整個Map,就是V的集合