玩轉資料結構——第六章:集合和對映
集合(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(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= |
1 | 2個節點 | 等比數列: a1*(1-q^n)/(a1-q1) |
2 | 4個節點 | |
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的集合