數據結構之集合與映射(一)

本篇主要內容:集合及其應用

集合Set

數學上定義集合有最基本的三個性質:確定性、互異性和無序性。所謂確定性就是一個元素是否在這個集合中是確定的,互異性是一個集合中不能有相同的元素,無序性是打亂集合中元素的順序集合還是這個集合。
在一般的數據結構教材中很少有講解集合(Set)這種數據結構,不過理解集合這種數據結構是很必要的,它是一種高階的數據結構,可以基於鏈表,二分搜索樹等來實現。
在java語言中實現集合這種數據結構,首先寫一個集合接口,集合可以由不同的方式實現:

'''Set.java'''
public interface Set<E> {
    void add(E e);
    void remove(E e);
    boolean contains(E e);
    int getSize();
    boolean isEmpty();
}

基於鏈表的實現
java提供的有鏈表類,不過爲了加深對一些操作的理解,這裏我們使用的是自己寫的鏈表類,它能實現增刪查改的基本功能,而且支持泛型:

'''LinkedList.java'''
public class LinkedList<E> {
    private class Node{
        //鏈表節點是鏈表內部類而且私有,用戶不需知道底層如何,屏蔽實現細節
        public E e;
        public Node next;
        public Node(E e,Node next){
            this.e = e;
            this.next = next;
        }
        public Node(E e){
            this(e,null);
        }
        public Node(){
            this(null,null);
        }
        @Override
        public String toString(){
            return e.toString();
        }
    }

    private Node dummyhead;
    private int size;

    public LinkedList(){
        dummyhead = new Node(null,null);
        size = 0;
    }

    //獲取鏈表中元素的個數
    public int getSize(){
        return size;
    }
    //返回鏈表是否爲空
    public boolean isEmpty(){
        return size == 0;
    }
    //在鏈表的index(0-based)位置添加新的元素e
    //鏈表中不是常用操作,用作測試和練習
    public void add(int index, E e){
        if(index < 0||index > size)
            throw new IllegalArgumentException("Add failed, illegal index");
        else{
            Node prev =dummyhead;
            for(int i = 0;i<index;i++)
                prev = prev.next;

            Node node = new Node(e);
            node.next = prev.next;
            prev.next = node;
            //prev.next = new Node(e,prev.next);

            size ++;
        }
    }
    //鏈表頭添加元素
    public void addFirst(E e){
        add(0,e);
    }
    //鏈表末尾添加元素e
    public void addLast(E e){
        add(size,e);
    }
    //鏈表中不是常用操作,用作測試和練習
    //獲得鏈表的第index個元素
    public E get(int index){
        if(index < 0||index > size)
            throw new IllegalArgumentException("Add failed, illegal index");
        Node cur = dummyhead.next;
        for(int i=0;i<index;i++)
            cur = cur.next;
        return cur.e;
    }
    //獲得鏈表的第一個元素
    public E getFirst(){
        return get(0);
    }
    //獲得鏈表的最後一個元素
    public E getLast(){
        return get(size-1);
    }
    //修改index位置的元素爲e
    //測試練習用
    public void set(int index,E e){
        //鏈表中不是常用操作,用作測試和練習
        if(index < 0||index > size)
            throw new IllegalArgumentException("Add failed, illegal index");
        Node cur = dummyhead.next;
        for(int i=0;i<index;i++)
            cur = cur.next;
        cur.e = e;
    }
    //查找鏈表是否存在元素e
    public boolean contains(E e){
        Node cur = dummyhead.next;
        while(cur != null){
            if(cur.e.equals(e))
                return true;
            cur = cur.next;
        }
        return false;
    }
    //刪除index位置的元素,不常用,做練習和測試
    public E remove(int index){
        if(index < 0||index > size)
            throw new IllegalArgumentException("Add failed, illegal index");
        Node prev = dummyhead;
        for(int i=0;i<index;i++)
            prev = prev.next;
        Node retNode = prev.next;
        prev.next = retNode.next;
        retNode.next = null;
        size --;

        return retNode.e;
    }
    //刪除鏈表第一個元素
    public E removeFirst(){
        return remove(0);
    }
    //刪除鏈表最後一個元素
    public E removeLast(){
        return remove(size-1);
    }
    //從鏈表中刪除元素e
    public void removeElement(E e){
        Node prev = dummyhead;
        while(prev.next!=null){
            if(prev.next.e.equals(e))
                break;
            prev = prev.next;
        }
        if(prev.next!=null){
            Node delNode = prev.next;
            prev.next = delNode.next;
            delNode.next = null;
            size --;
        }
    }
    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        Node cur = dummyhead.next;
        while(cur != null){
            res.append(cur + "->");
            cur = cur.next;
        }
        res.append("NULL");
        return res.toString();
    }
}

然後基於鏈表實現我們的集合,集合支持泛型:

public class LinkedListSet<E> implements Set<E> {
    private LinkedList<E> list;
    public LinkedListSet(){
        list = new LinkedList<>();
    }
    @Override
    public int getSize(){
        return list.getSize();
    }
    @Override
    public boolean isEmpty(){
        return list.isEmpty();
    }
    @Override
    public boolean contains(E e){
        return list.contains(e);
    }
    @Override
    public void add(E e){
        if(!list.contains(e))
            list.addFirst(e);
    }
    @Override
    public void remove(E e){
        list.removeElement(e);
    }
}

可以發現在實現鏈表後,集合中的增刪查操作只需調用它底層的鏈表相應操作就行。
基於BST的實現
BST的實現我們在《算法之美》系列文章遞歸的妙用中實現過了,這裏不再重複,下面給出基於BST的Set:

public class BSTSet<E extends Comparable<E>> implements Set<E> {
    private BST<E> bst;
    public BSTSet(){
        bst = new BST<>();
    }
    @Override
    public int getSize(){
        return bst.size();
    }
    @Override
    public boolean isEmpty(){
        return bst.isEmpty();
    }
    @Override
    public void add(E e){
        bst.add(e);
    }
    @Override
    public boolean contains(E e){
        return bst.contains(e);
    }
    @Override
    public void remove(E e){
        bst.remove(e);
    }
}

雖然兩種方式都能實現Set這種數據結構,但是兩者的性能存在巨大差異,基於鏈表實現的Set性能要遠遠低於基於BST實現的Set,這是爲什麼呢?這涉及到鏈表和BST的時間複雜度分析:

操作\方式 鏈表Set BSTSet
O(n) O(h)
O(n) O(h)
O(n) O(h)

對於我們實現的鏈表Set來說,不難得出它的增刪查時間複雜度都是O(n),這裏要說明的是本來鏈表的添加操作在頭部時間複雜度僅爲O(1),不過集合要求互異性,所以添加元素時還要檢查該元素是否已經存在,故最終添加操作的時間複雜度是O(n)。而對於BSTSet,它的時間複雜度和BST的深度有關,最壞的情況下BST會退化爲鏈表,時間複雜度也會是O(n),不過平均下來,BST的高度是log_2n,故BSTSet的平均時間複雜度是O(log_2n),這是遠遠優於O(n)的。如果Set是基於平衡二叉樹來實現,則最壞情況複雜度也是O(log_2n),這實際上就是java中Set的實現方式。


集合的應用

文本詞數統計

文本詞數統計有很廣泛的應用,比如詞數統計可以用於書的難度分級,一本有10000個不同單詞的書和一本有3000個不同單詞的書顯然不是一個難度級別。
現在我們就基於集合來實現一個txt英文小說詞數統計的小程序。首先編寫一個文件讀取類,這個類用於做分詞,將txt文本中英文單詞提取出來並全部轉爲小寫保存到String數組中:

import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.Locale;
import java.io.File;
import java.io.BufferedInputStream;
import java.io.IOException;
public class FileOperation {

    // 讀取filename中的內容,並將其中包含的所有詞語放進words中
    public static boolean readFile(String filename, ArrayList<String> words){

        if (filename == null || words == null){
            System.out.println("filename is null or words is null");
            return false;
        }
        // 文件讀取
        Scanner scanner;
        try {
            File file = new File(filename);
            if(file.exists()){
                FileInputStream fis = new FileInputStream(file);
                scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
                scanner.useLocale(Locale.ENGLISH);
            }
            else
                return false;
        }
        catch(IOException ioe){
            System.out.println("Cannot open " + filename);
            return false;
        }

        // 簡單分詞
        if (scanner.hasNextLine()) {

            String contents = scanner.useDelimiter("\\A").next();

            int start = firstCharacterIndex(contents, 0);
            for (int i = start + 1; i <= contents.length(); )
                if (i == contents.length() || !Character.isLetter(contents.charAt(i))) {
                    String word = contents.substring(start, i).toLowerCase();
                    words.add(word);
                    start = firstCharacterIndex(contents, i);
                    i = start + 1;
                } else
                    i++;
        }

        return true;
    }
    // 尋找字符串s中,從start的位置開始的第一個字母字符的位置
    private static int firstCharacterIndex(String s, int start){

        for( int i = start ; i < s.length() ; i ++ )
            if( Character.isLetter(s.charAt(i)) )
                return i;
        return s.length();
    }
}

這只是一個簡單的分詞過程,全部轉換爲小寫後單詞不同就算不同。有學過自然語言處理的可以更加細化一下分詞過程,比如將名詞的單複數算爲一個單詞,動詞的原型ing、ed、第三人稱單數算作一個單詞......不過這就比較麻煩了,本文不再進行這樣的處理。接下來使用我們的集合對Little Prince這部童話的英文版進行詞數統計:

'''Main.java'''
import java.util.ArrayList;
//BST比鏈表實現的集合更加高效
public class Main {
    private static double testSet(Set<String> set,String filename){
        long startTime = System.nanoTime();
        System.out.println("Little Prince");
        ArrayList<String> words = new ArrayList<>();
        if ((FileOperation.readFile(filename, words))) {
            System.out.println("Total words: " + words.size());
            for (String word : words)
                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) {
        String filename = "little-prince.txt";
        LinkedListSet<String> linkedListSet = new LinkedListSet<>();
        double time2 = testSet(linkedListSet,filename);
        System.out.println(time2+"\n");

        BSTSet<String> bstSet = new BSTSet<>();
        double time1 = testSet(bstSet,filename);
        System.out.println(time1+"\n");

        }
}

在上面的代碼中,我們用鏈表Set和BSTSet都進行了詞數統計任務,並對它們的過程計時,運行結果如下:

可以看到兩種方式都給出了正確的詞數統計,而且BSTSet方式明顯用時更少。

LeetCode804——唯一摩爾斯密碼詞

接下來用集合來解決一道LeetCode上的題目,題目描述如下:

這裏我們不再用自己的Set,而是用java中提供的TreeSet(底層是平衡二叉樹,性能更好)來實現我們的Solution:

import java.util.TreeSet;

class Solution {
    public int uniqueMorseRepresentations(String[] words) {
        String[] codes = {".-","-...","-.-.","-..",".","..-.","--.",
                "....","..",".---","-.-",".-..","--","-.","---",".--.",
                "--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."};
        TreeSet<String> set = new TreeSet<>();
        for(String word:words){
            StringBuilder res = new StringBuilder();
            for(int i=0;i<word.length();i++)
                res.append(codes[word.charAt(i)-'a']);//當前字符對應的摩斯碼存入res
            set.add(res.toString());
        }
        return set.size();
    }
}

提交,獲得通過!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章