返回文章列表

Rust 向量與集合操作詳解

本文探討 Rust 中向量(Vec)和集合型別的操作,包含刪除重複元素、合併、分割、排序、搜尋以及 HashMap、BTreeMap、VecDeque、LinkedList 和 BinaryHeap 的使用方法,並提供程式碼範例與解析,幫助讀者理解 Rust 中高效的資料結構操作技巧。

Rust 資料結構

Rust 提供了豐富的集合型別,讓開發者能靈活處理資料。Vec 作為動態陣列,支援高效的元素新增和刪除,並可透過 dedupconcatsplit_at 等方法進行去重、合併和分割。排序方面,sortsort_bysort_by_key 提供了彈性的排序策略。搜尋則可利用二分搜尋法快速定位元素。

除了 Vec 之外,Rust 的標準函式庫還提供了其他高效的資料結構。VecDeque 適合在佇列兩端進行增刪操作;LinkedList 適用於頻繁插入和刪除元素的場景;BinaryHeap 則適用於優先佇列的實作。HashMapBTreeMap 提供了高效的鍵值對儲存和查詢功能,HashMap 使用雜湊表,而 BTreeMap 使用樹狀結構,可以根據需求選擇合適的對映型別。Entry API 則簡化了對映中值的插入和更新操作,避免了重複的查詢。BTreeMap 還支援按鍵排序和分割等額外操作,提供更進階的資料處理能力。

向量(Vec)操作詳解:刪除重複元素、合併與分割

在Rust程式語言中,向量(Vec)是一種常用的資料結構,用於儲存多個相同型別的元素。本篇文章將介紹如何對向量進行刪除重複元素、合併與分割等操作。

刪除重複元素

Rust提供了.dedup()方法來刪除向量中相鄰的重複元素。然而,如果需要刪除所有重複元素(而不僅是相鄰的),有三種方法可供選擇:

  1. 排序後使用.dedup():先對向量進行排序,然後使用.dedup()方法。
  2. 使用HashSet:將向量的元素插入到一個HashSet中,由於HashSet不允許重複元素,這樣可以去除重複項。但這種方法會改變元素的順序。
  3. 使用.retain()方法:結合HashSet使用.retain()方法,可以在保持原來順序的同時去除重複元素。
let mut byte_vec = b"Misssssssissippi".to_vec();
let mut seen = std::collections::HashSet::new();
byte_vec.retain(|r| seen.insert(*r));
assert_eq!(&byte_vec, b"Misp");

此方法的原理是利用HashSet::insert()方法傳回false當插入的元素已經存在於集合中,從而達到去除重複元素的目的。

自定義刪除重複元素的規則

除了使用預設的.dedup()方法外,Rust還提供了.dedup_by().dedup_by_key()方法,允許自定義判斷兩個元素是否為重複的規則。

  • .dedup_by(same):使用提供的函式或閉包same來判斷兩個元素是否相等。
  • .dedup_by_key(key):根據提供的鍵值函式key來判斷兩個元素是否相等。
// 移除錯誤訊息中重複的錯誤
errors.dedup_by_key(|err| err.description().to_string());

合併(Joining)與分割(Splitting)

對於包含多個陣列或向量的資料結構,Rust提供了.concat().join()方法來合併這些子集合。

  • .concat():將多個切片合併成一個新的向量。
  • .join(&separator):與.concat()類別似,但會在每個子切片之間插入指定的分隔符。
assert_eq!([[1, 2], [3, 4], [5, 6]].concat(), vec![1, 2, 3, 4, 5, 6]);
assert_eq!([[1, 2], [3, 4], [5, 6]].join(&0), vec![1, 2, 0, 3, 4, 0, 5, 6]);

對於分割操作,Rust提供了多種方法,包括.split_at().split_first().split_last().split().splitn().rsplitn()等,用於將切片或向量分割成多個子切片。

程式碼範例與解析

let v = vec![0, 1, 2, 3];
let mid = v.len() / 2;
let (front_half, back_half) = v.split_at(mid);
assert_eq!(front_half, &[0, 1]);
assert_eq!(back_half, &[2, 3]);

內容解密:

此範例展示瞭如何使用.split_at()方法將一個向量分割成兩個子切片。v.split_at(mid)傳回一個元組,包含兩個切片:前半部分和後半部分。

交換(Swapping)與排序(Sorting)

Rust提供了.swap()方法來交換切片或向量中的兩個元素,以及.swap_remove()方法來高效地移除向量中的任意元素。

對於排序操作,Rust提供了.sort().sort_by()方法,分別用於對切片或向量進行預設的升序排序和自定義排序規則的排序。

let mut v = vec![3, 1, 2];
v.sort();
assert_eq!(v, vec![1, 2, 3]);

內容解密:

此範例展示瞭如何使用.sort()方法對向量進行升序排序。排序後,向量中的元素按照從小到大的順序排列。

Rust中的向量排序與搜尋

在Rust中,向量(Vec)是一種動態陣列,可以根據需要進行擴充或縮減。對向量進行排序和搜尋是常見的操作,Rust提供了多種方法來實作這些功能。

排序

Rust提供了三種方法對向量進行排序:sort_bysort_by_keysort

使用sort_by進行排序

sort_by方法允許您自定義比較函式來對向量進行排序。例如,若要根據結構體的某個欄位進行排序,可以使用以下程式碼:

students.sort_by(|a, b| a.last_name.cmp(&b.last_name));

若要根據多個欄位進行排序,可以比較元組(tuple):

students.sort_by(|a, b| {
    let a_key = (&a.last_name, &a.first_name);
    let b_key = (&b.last_name, &b.first_name);
    a_key.cmp(&b_key)
});

使用sort_by_key進行排序

sort_by_key方法根據指定的鍵值對向量進行排序。鍵值由提供的閉包(closure)計算得出。例如,若要根據學生的平均成績進行排序,可以使用以下程式碼:

students.sort_by_key(|s| s.grade_point_average());

內容解密:

  • sort_by_key方法需要一個閉包,該閉包傳回一個實作了Ord特徵的型別。
  • 鍵值函式可能會被呼叫超過n次,因此應避免在鍵值函式中進行昂貴的計算。

反向排序

若要進行反向排序,可以使用sort_by並交換比較的兩個引數,或者在排序後呼叫reverse方法:

slice.reverse();

搜尋

Rust提供了多種方法在已排序的向量中進行搜尋,包括binary_searchbinary_search_bybinary_search_by_key

使用二分搜尋

二分搜尋要求向量是已排序的。例如:

let index = slice.binary_search(&value);

若找到匹配的元素,則傳回Ok(index);否則,傳回Err(insertion_point),表示插入該元素的位置以保持排序順序。

內容解密:

  • 二分搜尋僅適用於已排序的向量。
  • 若向量未排序,二分搜尋的結果是未定義的。

比較切片

Rust支援對切片(slice)進行比較,前提是切片中的元素型別實作了PartialEqPartialOrd特徵。可以使用starts_withends_with方法檢查切片的字首或字尾是否與另一個切片匹配。

使用starts_withends_with

assert_eq!([1, 2, 3, 4].starts_with(&[1, 2]), true);
assert_eq!([1, 2, 3, 4].ends_with(&[3, 4]), true);

亂數元素

Rust的rand套件提供了從切片或向量中取得亂數元素或重新排列元素的方法。

使用chooseshuffle

use rand::{Rng, thread_rng};
let mut my_vec = vec![1, 2, 3, 4, 5];
thread_rng().shuffle(&mut my_vec);

Rust避免失效錯誤

Rust的設計避免了在迭代過程中修改集合導致的失效錯誤。例如,在Python中修改列表可能會導致意外的行為,而Rust在編譯時檢查並防止這種情況的發生。

Rust中的安全迭代

let mut my_vec = vec![1, 3, 5, 7, 9];
for val in &mut my_vec {
    if *val > 4 {
        *val = 0; // 安全地修改元素
    }
}

Rust 中的高效資料結構:VecDeque、LinkedList 與 BinaryHeap

在 Rust 程式語言中,除了常見的 Vec(向量)之外,還有其他幾種重要的資料結構能夠滿足不同的需求,包括 VecDequeLinkedListBinaryHeap。這些資料結構在特定的使用場景下能夠提供更高效的操作。

VecDeque:雙端佇列

VecDeque 是一種雙端佇列,支援在佇列的前端和後端進行高效的新增和移除操作。這使得它非常適合用於需要從兩端進行操作的場景,例如實作佇列或堆積疊。

主要操作

  • deque.push_front(value):在佇列的前端新增一個值。
  • deque.push_back(value):在佇列的後端新增一個值。
  • deque.pop_front():移除並傳回佇列的前端值,傳回 Option<T>,如果佇列為空則為 None
  • deque.pop_back():移除並傳回佇列的後端值,同樣傳回 Option<T>
  • deque.front()deque.back():傳回對佇列前端或後端元素的參照,傳回 Option<&T>
use std::collections::VecDeque;

let mut deque = VecDeque::new();
deque.push_back(1);
deque.push_front(2);
println!("{:?}", deque); // 輸出:[2, 1]

內容解密:

這段程式碼展示瞭如何使用 VecDeque 進行基本操作。首先,我們匯入了 std::collections::VecDeque 模組。然後,建立了一個新的 VecDeque 例項。透過 push_backpush_front 方法,分別在佇列的後端和前端增加了元素。最後,印出了佇列的內容。

LinkedList:鏈結串列

LinkedList 是一種鏈結串列,每個元素都儲存在單獨的堆積分配中。這種資料結構支援在序列的前端和後端進行操作,但不支援按照索引進行高效的存取。

主要操作

  • list.push_front(value)list.push_back(value):在串列的前端或後端新增元素。
  • list.pop_front()list.pop_back():移除並傳回串列的前端或後端元素。
use std::collections::LinkedList;

let mut list = LinkedList::new();
list.push_back(1);
list.push_front(2);
println!("{:?}", list); // 輸出:[2, 1]

內容解密:

這段程式碼展示瞭如何使用 LinkedList。首先,建立了一個新的 LinkedList 例項。然後,使用 push_backpush_front 方法在串列的後端和前端增加了元素。最後,印出了串列的內容。

BinaryHeap:二元堆積

BinaryHeap 是一種二元堆積,元素保持鬆散排序,以確保最大的值總是在堆積的前端。它常用於實作優先佇列。

主要操作

  • heap.push(value):將一個值新增到堆積中。
  • heap.pop():移除並傳回堆積中最大的值,傳回 Option<T>
  • heap.peek():傳回對堆積中最大值的參照,傳回 Option<&T>
use std::collections::BinaryHeap;

let mut heap = BinaryHeap::new();
heap.push(3);
heap.push(1);
heap.push(2);
println!("{:?}", heap.pop()); // 輸出:Some(3)

內容解密:

這段程式碼展示瞭如何使用 BinaryHeap。首先,建立了一個新的 BinaryHeap 例項。然後,使用 push 方法增加了幾個元素。最後,使用 pop 方法移除了並傳回了堆積中最大的值。

Rust中的對映型別:HashMap和BTreeMap

Rust提供了兩種對映(map)型別:HashMap<K, V>BTreeMap<K, V>。這兩種對映都用於儲存鍵值對,並且它們在鍵的查詢、插入和刪除操作上提供了高效的效能。

HashMap

HashMap使用雜湊表來儲存鍵值對,因此它要求鍵型別K必須實作HashEq這兩個特徵。HashMap的記憶體佈局如圖16-5所示,其中深灰色區域代表未使用的空間。所有的鍵、值和快取的雜湊碼都儲存在一個堆積分配的表中。當新增新的條目時,HashMap可能會分配一個更大的表並將所有資料移動到新的表中。

BTreeMap

BTreeMap則使用樹狀結構按照鍵的順序儲存條目,因此它要求鍵型別K必須實作Ord特徵。BTreeMap的記憶體佈局如圖16-6所示。與HashMap類別似,深灰色區域代表未使用的空間。BTreeMap將條目儲存在節點中,大多數節點只包含鍵值對,而非葉子節點還包含指向子節點的指標。

建立對映

有多種方法可以建立對映:

  • 使用HashMap::new()BTreeMap::new()來建立新的空對映。
  • 使用迭代器的collect()方法從鍵值對的迭代器中建立並填充新的對映。
  • 使用HashMap::with_capacity(n)來建立一個至少可以容納n個條目的空雜湊對映。

對映的操作

HashMapBTreeMap提供了許多相同的核心方法來操作鍵和值,包括:

  • map.len():傳回對映中的條目數量。
  • map.is_empty():如果對映為空,則傳回true。
  • map.contains_key(&key):如果對映包含給定鍵的條目,則傳回true。
  • map.get(&key):根據給定的鍵查詢對應的值。
  • map.insert(key, value):插入新的鍵值對,如果鍵已存在,則更新對應的值。
  • map.remove(&key):刪除給定鍵對應的條目。

條目(Entry)

兩種對映都有對應的Entry型別,用於減少重複的對映查詢。例如,可以使用entry()方法取得或建立一個學生記錄,這樣只需要進行一次查詢。

let record = student_map.entry(name.to_string()).or_insert_with(Student::new);

這行程式碼等價於先檢查是否存在對應的鍵,如果不存在則插入新的條目,然後取得該條目的可變參照。

內容解密:

  1. student_map.entry(name.to_string()):這裡呼叫了 entry() 方法,傳入了 name.to_string() 作為鍵,嘗試在 student_map 中查詢該鍵對應的條目。如果該鍵存在,則傳回一個 OccupiedEntry;如果不存在,則傳回一個 VacantEntry
  2. .or_insert_with(Student::new):這是對 Entry 的進一步操作。如果 Entry 是空的(即沒有對應的條目),則呼叫 Student::new() 建立一個新的 Student 例項,並將其插入到對映中;如果 Entry 已被佔用(即存在對應的條目),則直接傳回該條目的可變參照。
  3. 這種方式避免了多次查詢相同的鍵,提高了程式的效率。

BTreeMap的額外操作

由於BTreeMap按照鍵的順序儲存條目,因此它支援一些額外的操作,例如:

  • btree_map.split_at(&key):根據給定的鍵將對映分割成兩個對映。