[筆記](更新中)CSP-S 2024 查漏補缺

Sinktank發表於2024-08-25

複習內容部分來自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-=xp+=xp+xp-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<<" ";

其中,這四個方法返回位置的關係可以畫成下圖:

注意:如果在遍歷listvector等容器中途有刪除操作的話,一定要該寫遍歷格式:

for(auto it=lis.begin();it!=lis.end();){
	if(/* 條件 */){
        /* Something ... */
		it=lis.erase(it);
	}else{
        /* Something ... */
        it++;
    }
}

這是因為erase後迭代器會自動失效,而刪除方法返回的就是刪除元素的下一個元素。
\(4\)行寫作lis.erase(it++);也可以。

相關文章