指標在程式設計競賽中的缺點與替代辦法
指標是C/C++中的一個特性,號稱接近硬體、功能強大,可以完成許多其他語言不能完成的工作。然而,在程式設計競賽中,往往不需要這種底層的操作能力,更重要的是程式碼的清晰明瞭和除錯的方便快捷,因此這種情況下指標往往顯示出他不好的一面,所以我認為應當在程式設計競賽中避免使用不方便而且隱患重重的指標,而應該使用其他替代的方法,具體可能的情況如下。
1.“大”元素排序
常常遇到這樣一種情況,就是一個結構體面臨排序,然而一般的排序演算法(比如c++中的sort())都是基於交換實現的,所以空間比較大的元素排序所需時間也長,典型的做法就是對指向這些大元素的指標進行排序,用指標訪問元素,而且指標所佔空間小。類似於這種做法,可以新建一個整型陣列,賦值為1,2,3,..n,代表原陣列中對應下標的元素,然後,定義比較兩個整數的比較函式,用他們分別作下標就可以實現對原來結構體的排序。例如,下面這個程式對輸入的點座標先按x排序,再按y排序。
#include <cstdio> #include <algorithm> #include <iostream> using namespace std; struct P { int x,y; }q[105]; int cmp(int a,int b) { if (q[a].x<q[b].x||(q[a].x==q[b].x&&q[a].y<q[b].y)) return 1; else return 0; } int xb[105]; int main() { int n;//n<=100 scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%d%d",&q[i].x,&q[i].y); for (int i=1;i<=n;i++) xb[i]=i; sort(xb+1,xb+1+n,cmp); for (int i=1;i<=n;i++) { printf("%d %d\n",q[xb[i]].x,q[xb[i]].y); } return 0; }
這樣的話,完成了相同的功能,而且與指標不同的是,用下標表示元素更加可讀,也更好除錯,因為給你一個下標很容易就可以對應原陣列找到該元素,但如果是一個指標,就不能直接看指標看出他指的是誰。概括來講,一個下標不僅可以指向元素,還包含了序的資訊,而一個指標只能指向一個元素,要看出序的資訊,得做一次減法運算。
2.函式呼叫時地址傳遞
用指標的另一大場景就是函式呼叫時傳遞一個地址用於修改外部的變數,這樣就可以達到返回任何所需內容的效果。
比如高精度乘法時void add(bigint *a,bigint *b,bigint *c) {...}表示c=a+b,其實這種方式很常用,用習慣了過後也完全沒有問題,但是必須清楚,很多時候這種函式中a,b是要被讀取的,所以c就不能與a,b中任何一個相同,否則可能得到錯誤的結果。所以,這種情況下也有一種替代方案,就是把a,b,c全部搞成全域性變數,其中a,b是兩個加數,c是和:
bigint a,b,c;
void add() {...}就表示引數已經放在了a,b當中,函式執行完畢後結果儲存在了c當中,要呼叫函式的時候,就先把a,b給賦值好,再直接add(); 最後去把c中的內容取出來即可,這樣的話函式所需要的空間已經被開闢好了,就不會出現要讀取的內容被覆蓋的問題。這樣似乎是一種很不好的程式設計習慣,然而這樣的方法似乎在做題時很管用,避免了類似的錯誤。
類似的做法還有就是用全域性變數充當信使,避免用指標去傳遞他們。比如兩個函式之間,其中一個函式要用到另外一個函式中定義的東西,一般的做法當然是用引數傳遞過去,空間過大傳遞很慢就用指標傳地址,這個時候替代的做法就是直接把他們弄成全域性變數,這樣不管哪個函式都能用了。要傳引數,要返回一個值,要使用某一狀態變數都可以這麼幹。
3.動態記憶體分配
首先我覺得動態記憶體分配本身就是應該避免的(c++中的STL當然不用管,我說的是自己手動分配的),因為他確實很慢啊,而且要加快速度的話,也還是隻能一大塊一大塊的分配記憶體。第二,在類似連結串列一類的場景中,你多次分配,他們的空間不是連續的而是一小塊一小塊的,這不管是對於作業系統還是對於我們,都是不利的(就算是要全部初始化一次,都只能遍歷,逐個賦值,要是連在一大塊直接memset)。因此,與其一次一次的來分配,不如一次就分配夠,直接搞成全域性變數(區域性變數不能太大),這樣還可以全部初始化為0,對許多問題都方便許多。
再來談一談最典型的一個例子,就是連結串列的例子。首先就是弄個大陣列一次性開夠空間,然後就是用陣列的下標指向對應的元素而不是用指標,因為下標對人更加友好,容易看出他代表的元素是誰。接下來的操作就跟使用指標非常類似。
#include <cstdio>
#include <iostream>
using namespace std;
struct Node {
int val;
int next;
}lklist[105];//the number of elements should be less than 100
const int head=0;
int cnt;
int avai[105],ed;
void newlist() {
cnt=0;
ed=0;
}
int newitem() {
if (ed) return avai[ed--];
else return ++cnt;
}
void myinsert(int p,int x) {
int k=newitem();
lklist[k].val=x;
lklist[k].next=0;
int q=head;
for (int i=1;i<p;i++) q=lklist[q].next;
lklist[k].next=lklist[q].next;
lklist[q].next=k;
}
int getitem(int p) {
int q=head;
for (int i=1;i<=p;i++) q=lklist[q].next;
return lklist[q].val;
}
void mydel(int p) {
int q=head;
for (int i=1;i<p;i++) q=lklist[q].next;
avai[++ed]=lklist[q].next;
lklist[q].next=lklist[lklist[q].next].next;
}
int main() {
int m;
char op[5];
scanf("%d",&m);
int p,x;
for (int i=1;i<=m;i++) {
scanf("%s",op);
if (op[0]=='n') newlist();
else if (op[0]=='i') {
scanf("%d%d",&p,&x);
myinsert(p,x);
}
else if (op[0]=='g') {
scanf("%d",&p);
printf("%d\n",getitem(p));
}
else {
scanf("%d",&p);
mydel(p);
}
}
return 0;
}
其中head表示整個連結串列的頭,編號為0,表示空節點,其他任何元素的編號都不為0,刪除連結串列中的元素之後,用一個棧把刪除的節點的下標給存起來,留著以後新增元素時使用,就可以讓這些空間迴圈利用,減少浪費。當然如果刪除很少或者不刪除,那麼每次增加元素++cnt就好了。這樣寫連結串列就避免了使用指標,同時也避免了動態記憶體分配,除錯的時候也方便。
連結串列的一個重要應用是儲存圖的邊,針對每一個節點都建立一個連結串列,連結串列的每一個元素表示了這條邊的終點和邊的其他資訊(如邊權),只需簡單的讓多條連結串列共享一大塊記憶體空間即可,另外,還要使用頭插法來插入元素(O(1)的操作),這樣的方法就叫做鏈式前向星,其實本質上就是很多條連結串列共享了一大塊空間而已。
總而言之,在程式設計競賽中指標往往顯示出不便,而且有出錯的隱患,除錯起來也不太方便,所以應該用各種替代辦法避免使用指標。畢竟,也有許多人使用Java,而Java並沒有指標,所以指標並不是必需品,只是個人愛好罷了。