本篇主要內容:集合及其應用
集合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的高度是,故BSTSet的平均時間複雜度是,這是遠遠優於O(n)的。如果Set是基於平衡二叉樹來實現,則最壞情況複雜度也是,這實際上就是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();
}
}
提交,獲得通過!