一 數組簡介
數組
是一種基本的數據結構,用於按順序存儲元素的集合
。但是元素可以隨機存取,因爲數組中的每個元素都可以通過數組索引
來識別。
#include <iostream>
int main() {
// 1. Initialize
int a0[5];
int a1[5] = {1, 2, 3}; // other element will be set as the default value
// 2. Get Length
int size = sizeof(a1) / sizeof(*a1);
cout << "The size of a1 is: " << size << endl;
// 3. Access Element
cout << "The first element is: " << a1[0] << endl;
// 4. Iterate all Elements
cout << "[Version 1] The contents of a1 are:";
for (int i = 0; i < size; ++i) {
cout << " " << a1[i];
}
cout << endl;
cout << "[Version 2] The contents of a1 are:";
for (int& item: a1) {
cout << " " << item;
}
cout << endl;
// 5. Modify Element
a1[0] = 4;
// 6. Sort
sort(a1, a1 + size);
}
動態數組簡介
數組具有固定的容量
,需要在初始化時指定數組的大小。有時它會非常不方便並可能造成浪費。
因此,大多數編程語言都提供內置的動態數組
,它仍然是一個隨機存取的列表數據結構,但大小是可變的
。例如,在 C++ 中的 vector
,以及在 Java 中的 ArrayList
。
#include <iostream>
int main() {
// 1. initialize
vector<int> v0;
vector<int> v1(5, 0);
// 2. make a copy
vector<int> v2(v1.begin(), v1.end());
vector<int> v3(v2);
// 2. cast an array to a vector
int a[5] = {0, 1, 2, 3, 4};
vector<int> v4(a, *(&a + 1));
// 3. get length
cout << "The size of v4 is: " << v4.size() << endl;
// 4. access element
cout << "The first element in v4 is: " << v4[0] << endl;
// 5. iterate the vector
cout << "[Version 1] The contents of v4 are:";
for (int i = 0; i < v4.size(); ++i) {
cout << " " << v4[i];
}
cout << endl;
cout << "[Version 2] The contents of v4 are:";
for (int& item : v4) {
cout << " " << item;
}
cout << endl;
cout << "[Version 3] The contents of v4 are:";
for (auto item = v4.begin(); item != v4.end(); ++item) {
cout << " " << *item;
}
cout << endl;
// 6. modify element
v4[0] = 5;
// 7. sort
sort(v4.begin(), v4.end());
// 8. add new element at the end of the vector
v4.push_back(-1);
// 9. delete the last element
v4.pop_back();
}
二維數組簡介
類似於一維數組,二維數組
也是由元素的序列組成。但是這些元素可以排列在矩形網格中而不是直線上。
#include <iostream>
template <size_t n, size_t m>
void printArray(int (&a)[n][m]) {
for (int i = 0; i < n; ++i) { //n行m列
for (int j = 0; j < m; ++j) {
cout << a[i][j] << " ";
}
cout << endl;
}
}
int main() {
cout << "Example I:" << endl;
int a[2][5];
printArray(a);
cout << "Example II:" << endl;
int b[2][5] = {{1, 2, 3}};
printArray(b);
cout << "Example III:"<< endl;
int c[][5] = {1, 2, 3, 4, 5, 6, 7};
printArray(c);
cout << "Example IV:" << endl;
int d[][5] = {{1, 2, 3, 4}, {5, 6}, {7}};
printArray(d);
}
原理
在一些語言中,多維數組實際上是在內部
作爲一維數組實現的,而在其他一些語言中,實際上
根本沒有多維數組
。
1. C++ 將二維數組存儲爲一維數組。
下圖顯示了大小爲 M * N 的數組 A 的實際結構:
因此,如果我們將 A 定義爲也包含 M * N 個元素的一維數組,那麼實際上 A【i】【j】 就等於 A[i * N + j]。
2. 在Java中,二維數組實際上是包含着 M 個元素的一維數組,每個元素都是包含有 N 個整數的數組。
動態二維數組
與一維動態數組類似,我們也可以定義動態二維數組。 實際上,它可以只是一個嵌套的動態數組。
二 字符串簡介
字符串實際上是一個 unicode 字符
數組。可以執行幾乎所有我們在數組中使用的操作。
然而,二者之間還是存在一些區別。在這篇文章中,我們將介紹一些在處理字符串時應該注意的問題。這些特性在不同的語言之間可能有很大不同。
比較函數
字符串有它自己的比較函數
(我們將在下面的代碼中向你展示比較函數的用法)。
然而,存在這樣一個問題:
我們可以用 “==” 來比較兩個字符串嗎?
這取決於下面這個問題的答案:
我們使用的語言是否支持
運算符重載
?
- 如果答案是
yes
(例如 C++)。我們可以使用
“==” 來比較兩個字符串。 - 如果答案是
no
(例如 Java),我們可能無法使用
“” 來比較兩個字符串。當我們使用 “” 時,它實際上會比較這兩個對象是否是同一個對象。
#include <iostream>
int main() {
string s1 = "Hello World";
cout << "s1 is \"Hello World\"" << endl;
string s2 = s1;
cout << "s2 is initialized by s1" << endl;
string s3(s1);
cout << "s3 is initialized by s1" << endl;
// compare by '=='
cout << "Compared by '==':" << endl;
cout << "s1 and \"Hello World\": " << (s1 == "Hello World") << endl;
cout << "s1 and s2: " << (s1 == s2) << endl;
cout << "s1 and s3: " << (s1 == s3) << endl;
// compare by 'compare'
cout << "Compared by 'compare':" << endl;
cout << "s1 and \"Hello World\": " << !s1.compare("Hello World") << endl;
cout << "s1 and s2: " << !s1.compare(s2) << endl;
cout << "s1 and s3: " << !s1.compare(s3) << endl;
}
是否可變
不可變意味着一旦字符串被初始化,你就無法改變它的內容。
- 在某些語言(如 C ++)中,字符串是
可變的
。 也就是說,你可以像在數組中那樣修改字符串。 - 在其他一些語言(如 Java)中,字符串是不可變的。 此特性將帶來一些問題。 我們將在下一篇文章中闡明問題和解決方案。
你可以通過測試修改操作
來確定你喜歡的語言中的字符串是否可變。這裏有一個示例:
#include <iostream>
int main() {
string s1 = "Hello World";
s1[5] = ',';
cout << s1 << endl;
}
與數組相比,我們可以對字符串執行一些額外的操作。這裏有一些例子:
#include <iostream>
int main() {
string s1 = "Hello World";
// 1. concatenate
s1 += "!";
cout << s1 << endl;
// 2. find
cout << "The position of first 'o' is: " << s1.find('o') << endl;
cout << "The position of last 'o' is: " << s1.rfind('o') << endl;
// 3. get substr
cout << s1.substr(6, 5) << endl;
}
瞭解這些內置操作的時間複雜度。
例如,如果字符串的長度是 N
,那麼查找操作和子字符串操作的時間複雜度是 O(N)
。
在計算解決方案的時間複雜度時,不要忘記考慮內置操作的時間複雜度。
雙指針技巧 —— 情景一
通常,我們只使用從第一個元素開始並在最後一個元素結束的一個指針來進行迭代。 但是,有時候,我們可能需要同時使用兩個指針
來進行迭代。
讓我們從一個經典問題開始:
反轉數組中的元素。
其思想是將第一個元素與末尾進行交換,再向前移動到下一個元素,並不斷地交換,直到它到達中間位置。
我們可以同時使用兩個指針來完成迭代:一個從第一個元素開始
,另一個從最後一個元素開始
。持續交換它們所指向的元素,直到這兩個指針相遇。
void reverse(int *v, int N) {
int i = 0;
int j = N - 1;
while (i < j) {
swap(v[i], v[j]);
i++;
j--;
}
}
總結
總之,使用雙指針技巧的典型場景之一是你想要
從兩端向中間迭代數組。
這時你可以使用雙指針技巧:
一個指針從始端開始,而另一個指針從末端開始。
值得注意的是,這種技巧經常在排序
數組中使用。
雙指針技巧 —— 情景二
有時,我們可以使用兩個不同步的指針
來解決問題。
讓我們從另一個經典問題開始:
給定一個數組和一個值,原地刪除該值的所有實例並返回新的長度。
如果我們沒有空間複雜度上的限制,那就更容易了。我們可以初始化一個新的數組來存儲答案。如果元素不等於給定的目標值,則迭代原始數組並將元素添加到新的數組中。
實際上,它相當於使用了兩個指針,一個用於原始數組的迭代,另一個總是指向新數組的最後一個位置。
重新考慮空間限制
現在讓我們重新考慮空間受到限制的情況。
我們可以採用類似的策略,繼續使用兩個指針:一個仍然用於迭代
,而第二個指針總是指向下一次添加的位置
。
int removeElement(vector<int>& nums, int val) {
int k = 0;
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] != val) {
nums[k] = nums[i];
++k;
}
}
return k;
}
在上面的例子中,我們使用兩個指針,一個快指針 i
和一個慢指針 k
。i
每次移動一步,而 k
只在添加新的被需要的值時才移動一步。
總結
這是你需要使用雙指針技巧的一種非常常見的情況:
同時有一個慢指針和一個快指針。
解決這類問題的關鍵是
確定兩個指針的移動策略。
與前一個場景類似,你有時可能需要在使用雙指針技巧之前對數組進行排序,也可能需要運用貪心想法來決定你的運動策略。
數組相關的技術
- 這裏有一些其他類似於數組的數據結構,但具有一些不同的屬性:
-
正如我們所提到的,我們可以調用內置函數來對數組進行排序。但是,理解一些廣泛使用的排序算法的原理及其複雜度是很有用的。
-
二分查找也是一種重要的技術,用於在排序數組中搜索特定的元素。
-
我們在這一章中引入了雙指針技巧。想要靈活運用該技技巧是不容易的。這一技巧也可以用來解決:
- 鏈表中的慢指針和快指針問題
- 滑動窗口問題
如我們所提到的,我們可以調用內置函數來對數組進行排序。但是,理解一些廣泛使用的排序算法的原理及其複雜度是很有用的。
3. 二分查找也是一種重要的技術,用於在排序數組中搜索特定的元素。
- 我們在這一章中引入了雙指針技巧。想要靈活運用該技技巧是不容易的。這一技巧也可以用來解決:
- 鏈表中的慢指針和快指針問題
- 滑動窗口問題
- 雙指針技巧有時與貪心算法有關,它可以幫助我們設計指針的移動策略。