複習內容部分來自NOI大綱中入門級和提高階的內容。
聯合體(Union)
聯合體是一種複合資料型別,其的定義上與結構體的定義類似。
與結構體不同,聯合體中的所有元素共用一塊記憶體,所以它佔空間大小一般是最大成員的大小(不考慮對齊的情況下),相應地,任意時刻只有一個成員帶有值,如果訪問其他成員,得到的值可能有損壞。
對於不同的時刻需要使用不同型別的資料,且只使用剛賦值的成員的情況,無需開多種型別的變數,只需要用聯合體即可,更節省空間。
union node{
int a;
char b;
double c;
short d[5];
}t;
int main(){
cout<<sizeof(t)<<"\n";//Output : 16
//解釋:最大成員是short d[5],佔2*5=10位元組,
// 再對其到double的8位元組,即為16
t.a=100;
t.c=114514.191981;
t.d[3]=32767;
cout<<t.a<<" "<<t.c<<" "<<t.d[3]<<"\n";
//Output : 307931975 nan 32767
//解釋:因為是共用空間,所以一般最後賦值的位置才不被損壞
return 0;
}
原碼和補碼
我們用原碼錶示有符號整數,就是把二進位制的最高位作為符號位。拿\(8\)位整數舉例,\(-2\)的原碼是1000 0010
,\(5\)的原碼是0000 0101
。
然後我們可以發現一些問題:
- 原碼不能直接相加。比如\(-2+5\),按原碼計算
1000 0010
+0000 0101
得到10000111
,是十進位制的\(-7\)。 - 原碼的\(0\)有正負之分。這不僅是對空間的浪費,還會引發一系列衝突。
我們之所以要設計數在計算機裡的表示法,就是為了能夠方便地對它們進行運算。我們不妨先思考一下,什麼樣的表示法能支援數的相加。
首先我們要相加,那麼依照的就是無符號整數相加、自然溢位的邏輯,相應地就不存在符號位的概念。
根據相加的邏輯,我們很容易得出\(0\)應該表示為0000 0000
,\(1\)應該表示為0000 0001
……也就是說,非負數的表示和原碼一樣。
負數呢?這就可以用自然溢位嘛,\(-1\)就相當於\(+255\),\(-2\)就相當於\(+254\)……所以\(-1\sim -128\)分別表示為1111 1111
\(\sim\)1000 0000
。同時我們發現,\(\pm 0\)的問題也被解決了。
這種表示法就叫做補碼,計算機中有符號整數都是用它來儲存的。
和原碼對比一下:
雖然說補碼不存在符號位,但我們也可以用最高位來判斷正負,\(1\)是負數,\(0\)非負。
補碼的系統定義:
- 非負數的補碼就是原碼。
- 負數的補碼是原碼除符號位全部取反,再\(+1\)。
- 特別地,\(-128\)的補碼是
1000 0000
(這裡拿\(8\)位整數舉例,其他同理)。
理解起來也很簡單,\(x<0\)時,\(x\)的補碼是\(256+x=255+x+1\),其中“\(255+x\)”,就是把\(x\)的原碼除符號位外都取反。
然而\(-128\)沒有原碼,我們正好發現原來那個\(-0\)給空出來了,人為規定為\(-128\)。
一些筆記用“\(127+1=-128\)”作為例子來引入,實際上有符號整數的溢位是未定義行為:
#include<bits/stdc++.h> using namespace std; int x; int main(){ cin>>x; cout<<x+1<<"\n"; if(x+1<x) cout<<"overflow"; else cout<<"not overflow"; return 0; }
可以試試上面的程式碼,如果編譯器比較新,或者吸了氧,輸入
2147483647
時,可能輸出是-2147483647 not overflow
,原因是編譯器看到\(x+1<x\),知道只有溢位才可能為真,而溢位是未定義行為,所以直接認定它為假了(以上內容來自洛穀日報#265)。
補碼還有一個不錯的特性,補碼的補碼\(=\)原碼,能直接從表格觀察出來。證明:設\(x<0\),則\(x\)的補碼就是\(256+x\),作為原碼對應的數是\(-128-x\)(去掉最高位\(128\)再取反),\(x\)的補碼是\(-128-x\)的原碼,故得證。
哈夫曼編碼
對於一個字串,我們為了便於傳輸,常常把它編碼成一個01串。編碼方式有很多種,最簡單的就是用每個字元的ASCII碼值來編碼,這種編碼方式叫等長編碼,每個字元都用\(8\)位二進位制來表示。
雖然操作簡單,但傳輸效率卻不高,而哈夫曼編碼是變長編碼,它可以讓出現頻率更高的字元,編碼長度更短,同時不會出現衝突問題。
舉例子,AAAAABBBBCCCDDE
中,每種字母出現次數如下表所示:
A | B | C | D | E |
---|---|---|---|---|
5 | 4 | 3 | 2 | 1 |
我們按出現次數從小到大排序:
E | D | C | B | A |
---|---|---|---|---|
1 | 2 | 3 | 4 | 5 |
我們把字母看作樹上的節點,出現次數作為該點的權值,初始節點之間不相連。然後重複進行下面的操作:
- 取出森林裡權值最大的\(2\)個根節點,把它們作為一個新節點的左右兒子(順序無所謂),新節點的權值是兩點權值的和。
最後得到一棵樹,這就是哈夫曼樹:
根據構造過程,非葉子節點一定有\(2\)個子節點。我們用\(0\)和\(1\)分別表示連線左孩子的邊、連線右孩子的邊:
這樣每個葉子節點就有一個唯一的二進位制表示了,比如D
的表示就是001
,這就是每個字母的哈夫曼編碼。
哈夫曼編碼一定是最優編碼,因為編碼的總位數實際上是\(\sum\limits_{i=1}^{n} cnt[i]\times len[i]\),其中\(cnt[i],len[i]\)分別表示第\(i\)個字母出現次數和編碼長度。我們根據貪心的思想,一定把\(len\)最小的放在最下面,最大的放在最上面。
迭代器
迭代器是一種用於遍歷容器的指標。
迭代器有\(3\)中型別:前向迭代器,雙向迭代器,隨機訪問迭代器。
- 前向迭代器:只能單項移動,即
p++
、++p
,支援取值,賦值,可以用!=
、==
比較位置。 - 雙向迭代器:包含前向迭代器的功能,並支援
p--
、--p
。 - 隨機訪問迭代器:包含雙向迭代器的功能,並支援直接返回\(p\)的第\(i\)個元素的引用,支援
p-=x
、p+=x
、p+x
、p-x
,支援用>=
、<=
、>
、<
比較位置,還可以用兩個迭代器相減,得到它們下標的差。
不同容器支援的迭代器型別:
- 前向迭代器:unordered_map , unordered_multimap , unordered_set , unordered_multiset
- 雙向迭代器:list , set , multiset , map , multimap
- 隨機訪問迭代器:vector , deque , array
- 不支援迭代器:stack , queue
迭代器資料型別的定義:[type]::iterator it;
,例如map<string,int>::iterator it;
,有時可以直接用auto
代替,讓編譯器自動填充型別。
遍歷(用vector
舉例):
for(auto it=v.begin();it!=v.end();it++)
cout<<*it<<" ";
倒序遍歷需要把iterator
改成reverse_iterator
,迴圈寫成:
for(auto it=v.rbegin();it!=v.rend();it++)
cout<<*it<<" ";
其中,這四個方法返回位置的關係可以畫成下圖:
注意:如果在遍歷list
、vector
等容器中途有刪除操作的話,一定要該寫遍歷格式:
for(auto it=lis.begin();it!=lis.end();){
if(/* 條件 */){
/* Something ... */
it=lis.erase(it);
}else{
/* Something ... */
it++;
}
}
這是因為erase
後迭代器會自動失效,而刪除方法返回的就是刪除元素的下一個元素。
第\(4\)行寫作lis.erase(it++);
也可以。