㈠ Windows滑鼠屬性里「提高指針精確度」的功能是怎麼工作的
首先,這個功能的描述有錯誤,開啟這個功能之後,你的滑鼠移動速度和滑鼠指針移動速度不再是同時運動,而是加速度運動。
對於一般電腦用戶來說,這個功能可能適合長距離的滑鼠移動。但對於需要精準操作(游戲,制圖)的電腦用戶來說,就會非常影響。
以下是開啟windows滑鼠屬性「提高指針精準度」的步驟。
1、進入 控制面板所有控制面板項輕松訪問中心。
一:快速排序演算法
快速排序是由東尼·霍爾所發展的一種排序演算法。在平均狀況下,排序n個項目要Ο(nlogn)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(nlogn)演算法更快,因為它的內部循環(innerloop)可以在大部分的架構上很有效率地被實現出來。
快速排序使用分治法(Divideandconquer)策略來把一個串列(list)分為兩個子串列(sub-lists)。
演算法步驟:
1從數列中挑出一個元素,稱為「基準」(pivot),
2重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱為分區(partition)操作。
3遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
遞歸的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞歸下去,但是這個演算法總會退出,因為在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。
二:堆排序演算法
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序演算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
堆排序的平均時間復雜度為Ο(nlogn) 。
創建一個堆H[0..n-1]
把堆首(最大值)和堆尾互換
3.把堆的尺寸縮小1,並調用shift_down(0),目的是把新的數組頂端數據調整到相應位置
4.重復步驟2,直到堆的尺寸為1
三:歸並排序
歸並排序(Mergesort,台灣譯作:合並排序)是建立在歸並操作上的一種有效的排序演算法。該演算法是採用分治法(DivideandConquer)的一個非常典型的應用。
1.申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合並後的序列
2.設定兩個指針,最初位置分別為兩個已經排序序列的起始位置
3.比較兩個指針所指向的元素,選擇相對小的元素放入到合並空間,並移動指針到下一位置
4.重復步驟3直到某一指針達到序列尾
5.將另一序列剩下的所有元素直接復制到合並序列尾
四:二分查找演算法
二分查找演算法是一種在有序數組中查找某一特定元素的搜索演算法。搜素過程從數組的中間元素開始,如果中間元素正好是要查找的元素,則搜素過程結束;如果某一特定元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,而且跟開始一樣從中間元素開始比較。如果在某一步驟數組為空,則代表找不到。這種搜索演算法每一次比較都使搜索范圍縮小一半。折半搜索每次把搜索區域減少一半,時間復雜度為Ο(logn) 。
五:BFPRT(線性查找演算法)
BFPRT演算法解決的問題十分經典,即從某n個元素的序列中選出第k大(第k小)的元素,通過巧妙的分析,BFPRT可以保證在最壞情況下仍為線性時間復雜度。該演算法的思想與快速排序思想相似,當然,為使得演算法在最壞情況下,依然能達到o(n)的時間復雜度,五位演算法作者做了精妙的處理。
1.將n個元素每5個一組,分成n/5(上界)組。
2.取出每一組的中位數,任意排序方法,比如插入排序。
3.遞歸的調用selection演算法查找上一步中所有中位數的中位數,設為x,偶數個中位數的情況下設定為選取中間小的一個。
4.用x來分割數組,設小於等於x的個數為k,大於x的個數即為n-k。
5.若i==k,返回x;若i<k,在小於x的元素中遞歸查找第i小的元素;若i>k,在大於x的元素中遞歸查找第i-k小的元素。
終止條件:n=1時,返回的即是i小元素。
六:DFS(深度優先搜索)
深度優先搜索演算法(Depth-First-Search),是搜索演算法的一種。它沿著樹的深度遍歷樹的節點,盡可能深的搜索樹的分支。當節點v的所有邊都己被探尋過,搜索將回溯到發現節點v的那條邊的起始節點。這一過程一直進行到已發現從源節點可達的所有節點為止。如果還存在未被發現的節點,則選擇其中一個作為源節點並重復以上過程,整個進程反復進行直到所有節點都被訪問為止。DFS屬於盲目搜索。
深度優先搜索是圖論中的經典演算法,利用深度優先搜索演算法可以產生目標圖的相應拓撲排序表,利用拓撲排序表可以方便的解決很多相關的圖論問題,如最大路徑問題等等。一般用堆數據結構來輔助實現DFS演算法。
深度優先遍歷圖演算法步驟:
1.訪問頂點v;
2.依次從v的未被訪問的鄰接點出發,對圖進行深度優先遍歷;直至圖中和v有路徑相通的頂點都被訪問;
3.若此時圖中尚有頂點未被訪問,則從一個未被訪問的頂點出發,重新進行深度優先遍歷,直到圖中所有頂點均被訪問過為止。
上述描述可能比較抽象,舉個實例:
DFS在訪問圖中某一起始頂點v後,由v出發,訪問它的任一鄰接頂點w1;再從w1出發,訪問與w1鄰接但還沒有訪問過的頂點w2;然後再從w2出發,進行類似的訪問,…如此進行下去,直至到達所有的鄰接頂點都被訪問過的頂點u為止。
接著,退回一步,退到前一次剛訪問過的頂點,看是否還有其它沒有被訪問的鄰接頂點。如果有,則訪問此頂點,之後再從此頂點出發,進行與前述類似的訪問;如果沒有,就再退回一步進行搜索。重復上述過程,直到連通圖中所有頂點都被訪問過為止。
七:BFS(廣度優先搜索)
廣度優先搜索演算法(Breadth-First-Search),是一種圖形搜索演算法。簡單的說,BFS是從根節點開始,沿著樹(圖)的寬度遍歷樹(圖)的節點。如果所有節點均被訪問,則演算法中止。
BFS同樣屬於盲目搜索。一般用隊列數據結構來輔助實現BFS演算法。
1.首先將根節點放入隊列中。
2.從隊列中取出第一個節點,並檢驗它是否為目標。
如果找到目標,則結束搜尋並回傳結果。
否則將它所有尚未檢驗過的直接子節點加入隊列中。
3.若隊列為空,表示整張圖都檢查過了——亦即圖中沒有欲搜尋的目標。結束搜尋並回傳「找不到目標」。
4.重復步驟2。
八:Dijkstra演算法
戴克斯特拉演算法(Dijkstra』salgorithm)是由荷蘭計算機科學家艾茲赫爾·戴克斯特拉提出。迪科斯徹演算法使用了廣度優先搜索解決非負權有向圖的單源最短路徑問題,演算法最終得到一個最短路徑樹。該演算法常用於路由演算法或者作為其他圖演算法的一個子模塊。
該演算法的輸入包含了一個有權重的有向圖G,以及G中的一個來源頂點S。我們以V表示G中所有頂點的集合。每一個圖中的邊,都是兩個頂點所形成的有序元素對。(u,v)表示從頂點u到v有路徑相連。我們以E表示G中所有邊的集合,而邊的權重則由權重函數w:E→[0,∞]定義。因此,w(u,v)就是從頂點u到頂點v的非負權重(weight)。邊的權重可以想像成兩個頂點之間的距離。任兩點間路徑的權重,就是該路徑上所有邊的權重總和。已知有V中有頂點s及t,Dijkstra演算法可以找到s到t的最低權重路徑(例如,最短路徑)。這個演算法也可以在一個圖中,找到從一個頂點s到任何其他頂點的最短路徑。對於不含負權的有向圖,Dijkstra演算法是目前已知的最快的單源最短路徑演算法。
1.初始時令S=,T=,T中頂點對應的距離值
若存在<V0,Vi>,d(V0,Vi)為<V0,Vi>弧上的權值
若不存在<V0,Vi>,d(V0,Vi)為∞
2.從T中選取一個其距離值為最小的頂點W且不在S中,加入S
3.對其餘T中頂點的距離值進行修改:若加進W作中間頂點,從V0到Vi的距離值縮短,則修改此距離值
重復上述步驟2、3,直到S中包含所有頂點,即W=Vi為止
九:動態規劃演算法
動態規劃(Dynamicprogramming)是一種在數學、計算機科學和經濟學中使用的,通過把原問題分解為相對簡單的子問題的方式求解復雜問題的方法。動態規劃常常適用於有重疊子問題和最優子結構性質的問題,動態規劃方法所耗時間往往遠少於樸素解法。
動態規劃背後的基本思想非常簡單。大致上,若要解一個給定問題,我們需要解其不同部分(即子問題),再合並子問題的解以得出原問題的解。通常許多子問題非常相似,為此動態規劃法試圖僅僅解決每個子問題一次,從而減少計算量:一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次需要同一個子問題解之時直接查表。這種做法在重復子問題的數目關於輸入的規模呈指數增長時特別有用。
關於動態規劃最經典的問題當屬背包問題。
1.最優子結構性質。如果問題的最優解所包含的子問題的解也是最優的,我們就稱該問題具有最優子結構性質(即滿足最優化原理)。最優子結構性質為動態規劃演算法解決問題提供了重要線索。
2.子問題重疊性質。子問題重疊性質是指在用遞歸演算法自頂向下對問題進行求解時,每次產生的子問題並不總是新問題,有些子問題會被重復計算多次。動態規劃演算法正是利用了這種子問題的重疊性質,對每一個子問題只計算一次,然後將其計算結果保存在一個表格中,當再次需要計算已經計算過的子問題時,只是在表格中簡單地查看一下結果,從而獲得較高的效率。
十:樸素貝葉斯分類演算法
樸素貝葉斯分類演算法是一種基於貝葉斯定理的簡單概率分類演算法。貝葉斯分類的基礎是概率推理,就是在各種條件的存在不確定,僅知其出現概率的情況下,如何完成推理和決策任務。概率推理是與確定性推理相對應的。而樸素貝葉斯分類器是基於獨立假設的,即假設樣本每個特徵與其他特徵都不相關。
樸素貝葉斯分類器依靠精確的自然概率模型,在有監督學習的樣本集中能獲取得非常好的分類效果。在許多實際應用中,樸素貝葉斯模型參數估計使用最大似然估計方法,換言樸素貝葉斯模型能工作並沒有用到貝葉斯概率或者任何貝葉斯模型。
盡管是帶著這些樸素思想和過於簡單化的假設,但樸素貝葉斯分類器在很多復雜的現實情形中仍能夠取得相當好的效果。
通過掌握以上演算法,能夠幫你迅速提高編程能力,成為一名優秀的程序員。
㈢ 衡量演算法效率的方法與准則
演算法效率與分析
數據結構作為程序設計的基礎,其對演算法效率的影響必然是不可忽視的。本文就如何合理選擇數據結構來優化演算法這一問題,對選擇數據結構的原則和方法進行了一些探討。首先對數據邏輯結構的重要性進行了分析,提出了選擇邏輯結構的兩個基本原則;接著又比較了順序和鏈式兩種存儲結構的優點和缺點,並討論了選擇數據存儲結構的方法;最後本文從選擇數據結構的的另一角度出發,進一步探討了如何將多種數據結構進行結合的方法。在討論方法的同時,本文還結合實際,選用了一些較具有代表性的信息學競賽試題舉例進行了分析
【正文】一、引論
「數據結構+演算法=程序」,這就說明程序設計的實質就是對確定的問題選擇一種合適的數據結構,加上設計一種好的演算法。由此可見,數據結構在程序設計中有著十分重要的地位。
數據結構是相互之間存在一種或多種特定關系的數據元素的集合。因為這其中的「關系」,指的是數據元素之間的邏輯關系,因此數據結構又稱為數據的邏輯結構。而相對於邏輯結構這個比較抽象的概念,我們將數據結構在計算機中的表示又稱為數據的存儲結構。
建立問題的數學模型,進而設計問題的演算法,直至編出程序並進行調試通過,這就是我們解決信息學問題的一般步驟。我們要建立問題的數學模型,必須首先找出問題中各對象之間的關系,也就是確定所使用的邏輯結構;同時,設計演算法和程序實現的過程,必須確定如何實現對各個對象的操作,而操作的方法是決定於數據所採用的存儲結構的。因此,數據邏輯結構和存儲結構的好壞,將直接影響到程序的效率。
二、選擇合理的邏輯結構
在程序設計中,邏輯結構的選用就是要分析題目中的數據元素之間的關系,並根據這些特定關系來選用合適的邏輯結構以實現對問題的數學描述,進一步解決問題。邏輯結構實際上是用數學的方法來描述問題中所涉及的操作對象及對象之間的關系,將操作對象抽象為數學元素,將對象之間的復雜關系用數學語言描述出來。
根據數據元素之間關系的不同特性,通常有以下四種基本邏輯結構:集合、線性結構、樹形結構、圖狀(網狀)結構。這四種結構中,除了集合中的數據元素之間只有「同屬於一個集合」的關系外,其它三種結構數據元素之間分別為「一對一」、「一對多」、「多對多」的關系。
因此,在選擇邏輯結構之前,我們應首先把題目中的操作對象和對象之間的關系分析清楚,然後再根據這些關系的特點來合理的選用邏輯結構。尤其是在某些復雜的問題中,數據之間的關系相當復雜,且選用不同邏輯結構都可以解決這一問題,但選用不同邏輯結構實現的演算法效率大不一樣。
對於這一類問題,我們應採用怎樣的標准對邏輯結構進行選擇呢?
下文將探討選擇合理邏輯結構應充分考慮的兩個因素。
一、 充分利用「可直接使用」的信息。
首先,我們這里所講的「信息」,指的是元素與元素之間的關系。
對於待處理的信息,大致可分為「可直接使用」和「不可直接使用」兩類。對於「可直接使用」的信息,我們使用時十分方便,只需直接拿來就可以了。而對於「不可直接使用」的這一類,我們也可以通過某些間接的方式,使之成為可以使用的信息,但其中轉化的過程顯然是比較浪費時間的。
由此可見,我們所需要的是盡量多的「可直接使用」的信息。這樣的信息越多,演算法的效率就會越高。
對於不同的邏輯結構,其包含的信息是不同的,演算法對信息的利用也會出現不同的復雜程度。因此,要使演算法能夠充分利用「可直接使用」的信息,而避免演算法在信息由「不可直接使用」向「可直接使用」的轉化過程中浪費過多的時間,我們必然需要採用一種合理的邏輯結構,使其包含更多「可直接使用」的信息。
〖問題一〗 IOI99的《隱藏的碼字》。
〖問題描述〗
問題中給出了一些碼字和一個文本,要求編程找出文本中包含這些碼字的所有項目,並將找出的項目組成一個最優的「答案」,使得答案中各項目所包含的碼字長度總和最大。每一個項目包括一個碼字,以及該碼字在文本中的一個覆蓋序列(如』abcadc』就是碼字』abac』的一個覆蓋序列),並且覆蓋序列的長度不超過1000。同時,「答案」要求其中每個項目的覆蓋序列互相沒有重疊。
〖問題分析〗
對於此題,一種較容易得出的基本演算法是:對覆蓋序列在文本中的終止位置進行循環,再判斷包含了哪些碼字,找出所有項目,並最後使用動態規劃的方法將項目組成最優的「答案」。
演算法的其它方面我們暫且不做考慮,而先對問題所採用的邏輯結構進行選擇。
如果我們採用線性的邏輯結構(如循環隊列),那麼我們在判斷是否包含某個碼字t時,所用的方法為:初始時用指針p指向終止位置,接著通過p的不斷前移,依次找出碼字t從尾到頭的各個字母。例如碼字為「ABDCAB」,而文本圖1-1,終止位置為最右邊的箭頭符號,每個箭頭代表依次找到的碼字的各個字母。
指針p的移動方向
A B D C A B
C D A C B D C A D C D B A D C C B A D
圖1-1
由於題目規定碼字的覆蓋序列長度不超過1000,所以進行這樣的一次是否包含的判斷,其復雜度為O(1000)。
由於碼字t中相鄰兩字母在文本中的位置,並非只有相鄰(如圖1-1中的』D』和』C』)這一種關系,中間還可能間隔了許多的字母(如圖1-1中』C』和』A』就間隔了2個字母),而線性結構中擁有的信息,僅僅只存在於相鄰的兩元素之間。通過這樣簡單的信息來尋找碼字的某一個字母,其效率顯然不高。
如果我們建立一個有向圖,其中頂點i(即文本的第i位)用52條弧分別連接』a』..』z』,』A』..』Z』這52個字母在i位以前最後出現的位置(如圖1-2的連接方式),我們要尋找碼字中某個字母的前一個字母,就可以直接利用已連接的邊,而不需用枚舉的方法。我們也可以把問題看為:從有向圖的一個頂點出發,尋找一條長度為length(t)-1的路徑,並且路徑中經過的頂點,按照碼字t中的字母有序。
C D A C B D C A D C D B A D C C B A D
圖1-2
通過計算,用圖進行記錄在空間上完全可以承受(記錄1000個點×52條弧×4位元組的長整型=200k左右)。在時間上,由於可以充分利用第i位和第i+1位弧的連接方式變化不大這一點(如圖1-2所示,第i位和第i+1位只有一條弧的指向發生了變化,即第i+1位將其中一條弧指向了第i位),所以要對圖中的弧進行記錄,只需對弧的指向進行整體賦值,並改變其中的某一條弧即可。
因此,我們通過採用圖的邏輯結構,使得尋找字母的效率大大提高,其判斷的復雜度為O(length(t)),最壞為O(100),比原來方法的判斷效率提高了10倍。
(附程序codes.pas)
對於這個例子,雖然用線性的數據結構也可以解決,但由於判斷的特殊性,每次需要的信息並不能從相鄰的元素中找到,而線性結構中只有相鄰元素之間存在關系的這一點,就成為了一個很明顯的缺點。因此,問題一線性結構中的信息,就屬於「不可直接使用」的信息。相對而言,圖的結構就正好滿足了我們的需要,將所有可能產生關系的點都用弧連接起來,使我們可以利用弧的關系,高效地進行判斷尋找的過程。雖然圖的結構更加復雜,但卻將「不可直接使用」的信息,轉化成為了「可直接使用」的信息,演算法效率的提高,自然在情理之中。。
二、 不記錄「無用」信息。
從問題一中我們看到,由於圖結構的信息量大,所以其中的信息基本上都是「可用」的。但是,這並不表示我們就一定要使用圖的結構。在某些情況下,圖結構中的「可用」信息,是有些多餘的。
信息都「可用」自然是好事,但倘若其中「無用」(不需要)的信息太多,就只會增加我們思考分析和處理問題時的復雜程度,反而不利於我們解決問題了。
〖問題二〗 湖南省1997年組隊賽的《乘船問題》
〖問題描述〗
有N個人需要乘船,而每船最多隻能載兩人,且必須同名或同姓。求最少需要多少條船。
〖問題分析〗
看到這道題,很多人都會想到圖的數據結構:將N個人看作無向圖的N個點,凡同名或同姓的人之間都連上邊。
要滿足用船最少的條件,就是需要盡量多的兩人共乘一條船,表現在圖中就是要用最少的邊完成對所有頂點的覆蓋。這就正好對應了圖論的典型問題:求最小邊的覆蓋。所用的演算法為「求任意圖最大匹配」的演算法。
使用「求任意圖最大匹配」的演算法比較復雜(要用到擴展交錯樹,對花的收縮等等),效率也不是很高。因此,我們必須尋找一個更簡單高效的方法。
首先,由於圖中任兩個連通分量都是相對獨立的,也就是說任一條匹配邊的兩頂點,都只屬於同一個連通分量。因此,我們可以對每個連通分量分別進行處理,而不會影響最終的結果。
同時,我們還可以對需要船隻s的下限進行估計:
對於一個包含Pi個頂點的連通分量,其最小覆蓋邊數顯然為[Pi/2]。若圖中共有L個連通分量,則s=∑[Pi/2](1<=i<=L)。
然後,我們通過多次嘗試,可得出一個猜想:
實際需要的覆蓋邊數完全等於我們求出的下限∑[Pi/2](1<=i<=L)。
要用圖的結構對上述猜想進行證明,可參照以下兩步進行:
1. 連通分量中若不存在度為1的點,就必然存在迴路。
2. 從圖中刪去度為1的點及其相鄰的點,或刪去迴路中的任何一邊,連通分量依然連通,即連通分量必然存在非橋邊。
由於圖的方法不是這里的重點,所以具體證明不做詳述。而由採用圖的數據結構得出的演算法為:每次輸出一條非橋的邊,並從圖中將邊的兩頂點刪去。此演算法的時間復雜度為O(n3)。(尋找一條非橋邊的復雜度為O(n2),尋找覆蓋邊操作的復雜度為O(n))
由於受到圖結構的限制,時間復雜度已經無法降低,所以如果我們要繼續對演算法進行優化,只有考慮使用另一種邏輯結構。這里,我想到了使用二叉樹的結構,具體說就是將圖中的連通分量都轉化為二叉樹,用二叉樹來解決問題。
首先,我們以連通分量中任一個頂點作為樹根,然後我們來確定建樹的方法。
1. 找出與根結點i同姓的點j(j不在二叉樹中)作為i的左兒子,再以j為樹根建立子樹。
2. 找出與根結點i同名的點k(k不在二叉樹中)作為i的右兒子,再以k為樹根建立子樹。
如圖2-1-1中的連通分量,我們通過上面的建樹方法,可以使其成為圖2-1-2中的二叉樹的結構(以結點1為根)。(兩點間用實線表示同姓,虛線表示同名)
圖2-1-2
圖2-1-1
接著,我就來證明這棵樹一定包含了連通分量中的所有頂點。
【引理2.1】
若二叉樹T中包含了某個結點p,那麼連通分量中所有與p同姓的點一定都在T中。
證明:
為了論證的方便,我們約定:s表示與p同姓的頂點集合;lc[p,0]表示結點p,lc[p,i](i>0)表示lc[p,i-1]的左兒子,顯然lc[p,i]與p是同姓的。
假設存在某個點q,滿足qs且qT。由於s是有限集合,因而必然存在某個lc[p,k]無左兒子。則我們可以令lc[p,k+1]=q,所以qT,與假設qT相矛盾。
所以假設不成立,原命題得證。
由引理2.1的證明方法,我們同理可證引理2.2。
【引理2.2】
若二叉樹T中包含了某個結點p,那麼連通分量中所有與p同名的點一定都在T中。
有了上面的兩個引理,我們就不難得出下面的定理了。
【定理一】
以連通分量中的任一點p作為根結點的二叉樹,必然能夠包含連通分量中的所有頂點。
證明:
由引理2.1和引理2.2,所有與p同姓或同名的點都一定在二叉樹中,即連通分量中所有與p有邊相連的點都在二叉樹中。由連通分量中任兩點間都存在路徑的特性,該連通分量中的所有點都在二叉樹中。
在證明二叉樹中包含了連通分量的所有頂點後,我們接著就需要證明我們的猜想,也就是下面的定理:
【定理二】包含m個結點的二叉樹Tm,只需要船的數量為boat[m]=[m/2](mN)。
證明:
(i) 當m=1,m=2,m=3時命題顯然成立。
圖2-2-1
圖2-2-2
圖2-2-3
(ii) 假設當m<k(k>3)時命題成立,那麼當m=k時,我們首先從樹中找到一個層次最深的結點,並假設這個結點的父親為p。那麼,此時有且只有以下三種情況(結點中帶有陰影的是p結點):
(1) 如圖2-2-1,p只有一個兒子。此時刪去p和p唯一的兒子,Tk就成為了Tk-2,則boat[k]=boat[k-2]+1=[(k-2)/2]+1=[k/2]。
(2) 如圖2-2-2,p有兩個兒子,並且p是其父親的左兒子。此時可刪去p和p的右兒子,並可將p的左兒子放到p的位置上。同樣地,Tk成為了Tk-2,boat[k]=boat[k-2]+1=[k/2]。
(3) 如圖2-2-3,p有兩個兒子,並且p是其父親的右兒子。此時可刪去p和p的左兒子,並可將p的右兒子放到p的位置上。情況與(2)十分相似,易得此時得boat[k]=boat[k-2]+1=[k/2]。
綜合(1)、(2)、(3),當m=k時,boat[k]=[k/2]。
最後,綜合(i)、(ii),對於一切mN,boat[m]=[m/2]。
由上述證明,我們將問題中數據的圖結構轉化為樹結構後,可以得出求一棵二叉樹的乘船方案的演算法:
proc try(father:integer;var root:integer;var rest:byte);
{輸出root為樹根的子樹的乘船方案,father=0表示root是其父親的左兒子,
father=1表示root是其父親的右兒子,rest表示輸出子樹的乘船方案後,
是否還剩下一個根結點未乘船}
begin
visit[root]:=true; {標記root已訪問}
找到一個與root同姓且未訪問的結點j;
if j<>n+1 then try(0,j,lrest);
找到一個與root同姓且未訪問的結點k;
if k<>n+1 then try(1,k,rrest);
if (lrest=1) xor (rrest=1) then begin {判斷root是否只有一個兒子,情況一}
if lrest=1 then print(lrest,root) else print(rrest,root);
rest:=0;
end
else if (lrest=1) and (rrest=1) then begin {判斷root是否有兩個兒子}
if father=0 then begin
print(rrest,root);root:=j; {情況二}
end
else begin
print(lrest,root);root:=k; {情況三}
end;
rest:=1;
end
else rest:=1;
end;
這只是輸出一棵二叉樹的乘船方案的演算法,要輸出所有人的乘船方案,我們還需再加一層循環,用於尋找各棵二叉樹的根結點,但由於每個點都只會訪問一次,尋找其左右兒子各需進行一次循環,所以演算法的時間復雜度為O(n2)。(附程序boat.pas)
最後,我們對兩種結構得出不同時間復雜度演算法的原因進行分析。其中最關鍵的一點就是因為二叉樹雖然結構相對較簡單,但已經包含了幾乎全部都「有用」的信息。由我們尋找乘船方案的演算法可知,二叉樹中的所有邊不僅都發揮了作用,而且沒有重復的使用,可見信息的利用率也是相當之高的。
既然採用樹結構已經足夠,圖結構中的一些信息就顯然就成為了「無用」的信息。這些多餘的「無用」信息,使我們在分析問題時難於發現規律,也很難找到高效的演算法進行解決。這正如迷宮中的牆一樣,越多越難走。「無用」的信息,只會干擾問題的規律性,使我們更難找出解決問題的方法。
小結
我們對數據的邏輯結構進行選擇,是構造數學模型一大關鍵,而演算法又是用來解決數學模型的。要使演算法效率高,首先必須選好數據的邏輯結構。上面已經提出了選擇邏輯結構的兩個條件(思考方向),總之目的是提高信息的利用效果。利用「可直接使用」的信息,由於中間不需其它操作,利用的效率自然很高;不不記錄「無用」的信息,就會使我們更加專心地研究分析「有用」的信息,對信息的使用也必然會更加優化。
總之,在解決問題的過程中,選擇合理的邏輯結構是相當重要的環
三、 選擇合理的存儲結構
數據的存儲結構,分為順序存儲結構和鏈式存儲結構。順序存儲結構的特點是藉助元素在存儲器中的相對位置來表示數據元素之間的邏輯關系;鏈式存儲結構則是藉助指示元素存儲地址的指針表示數據元素之間的邏輯關系。
因為兩種存儲結構的不同,導致這兩種存儲結構在具體使用時也分別存在著優點和缺點。
這里有一個較簡單的例子:我們需要記錄一個n×n的矩陣,矩陣中包含的非0元素為m個。
此時,我們若採用順序存儲結構,就會使用一個n×n的二維數組,將所有數據元素全部記錄下來;若採用鏈式存儲結構,則需要使用一個包含m個結點的鏈表,記錄所有非0的m個數據元素。由這樣兩種不同的記錄方式,我們可以通過對數據的不同操作來分析它們的優點和缺點。
1. 隨機訪問矩陣中任意元素。由於順序結構在物理位置上是相鄰的,所以可以很容易地獲得任意元素的存儲地址,其復雜度為O(1);對於鏈式結構,由於不具備物理位置相鄰的特點,所以首先必須對整個鏈表進行一次遍歷,尋找需進行訪問的元素的存儲地址,其復雜度為O(m)。此時使用順序結構顯然效率更高。
2. 對所有數據進行遍歷。兩種存儲結構對於這種操作的復雜度是顯而易見的,順序結構的復雜度為O(n2),鏈式結構為O(m)。由於在一般情況下m要遠小於n2,所以此時鏈式結構的效率要高上許多。
除上述兩種操作外,對於其它的操作,這兩種結構都不存在很明顯的優點和缺點,如對鏈表進行刪除或插入操作,在順序結構中可表示為改變相應位置的數據元素。
既然兩種存儲結構對於不同的操作,其效率存在較大的差異,那麼我們在確定存儲結構時,必須仔細分析演算法中操作的需要,合理地選擇一種能夠「揚長避短」的存儲結構。
一、合理採用順序存儲結構。
我們在平常做題時,大多都是使用順序存儲結構對數據進行存儲。究其原因,一方面是出於順序結構操作方便的考慮,另一方面是在程序實現的過程中,使用順序結構相對於鏈式結構更便於對程序進行調試和查找錯誤。因此,大多數人習慣上認為,能夠使用順序結構進行存儲的問題,最「好」採用順序存儲結構。
其實,這個所謂的「好」只是一個相對的標准,是建立在以下兩個前提條件之下的:
1. 鏈式結構存儲的結點與順序結構存儲的結點數目相差不大。這種情況下,由於存儲的結點數目比較接近,使用鏈式結構完全不能體現出記錄結點少的優點,並且可能會由於指針操作較慢而降低演算法的效率。更有甚者,由於指針自身佔用的空間較大,且結點數目較多,因而演算法對空間的要求可能根本無法得到滿足。
2. 並非演算法效率的瓶頸所在。由於不是演算法最費時間的地方,這里是否進行改進,顯然是不會對整個演算法構成太大影響的,若使用鏈式結構反而會顯得操作過於繁瑣。
二、必要時採用鏈式存儲結構。
上面我對使用順序存儲結構的條件進行了分析,最後就只剩下何時應該採用鏈式存儲結構的問題了。
由於鏈式結構中指針操作確實較繁瑣,並且速度也較慢,調試也不方便,因而大家一般都不太願意用鏈式的存儲結構。但是,這只是一般的觀點,當鏈式結構確實對演算法有很大改進時,我們還是不得不進行考慮的。
〖問題三〗 IOI99的《地下城市》。
〖問題描述〗
已知一個城市的地圖,但未給出你的初始位置。你需要通過一系列的移動和探索,以確定初始時所在的位置。題目的限制是:
1. 不能移動到有牆的方格。
2. 只能探索當前所在位置四個方向上的相鄰方格。
在這兩個限制條件下,要求我們的探索次數(不包括移動)盡可能的少。
〖問題分析〗
由於存儲結構要由演算法的需要確定,因此我們首先來確定問題的演算法。
經過對問題的分析,我們得出解題的基本思想:先假設所有無牆的方格都可能是初始位置,再通過探索一步步地縮小初始位置的范圍,最終得到真正的初始位置。同時,為提高演算法效率,我們還用到了分治的思想,使我們每一次探索都盡量多的縮小初始位置的范圍(使程序盡量減少對運氣的依賴)。
接著,我們來確定此題的存儲結構。
由於這道題的地圖是一個二維的矩陣,所以一般來講,採用順序存儲結構理所當然。但是,順序存儲結構在這道題中暴露了很大的缺點。我們所進行的最多的操作,一是對初始位置的范圍進行篩選,二是判斷要選擇哪個位置進行探索。而這兩種操作,所需要用到的數據,只是龐大地圖中很少的一部分。如果採用順序存儲結構(如圖3-1中陰影部分表示已標記),無論你需要用到多少數據,始終都要完全的遍歷整個地圖。
4
3
2
1
1 2 3 4
圖3-1
head
圖3-2
然而,如果我們採用的是鏈式存儲結構(如圖3-2的鏈表),那麼我們需要多少數據,就只會遍歷多少數據,這樣不僅充分發揮了鏈式存儲結構的優點,而且由於不需單獨對某一個數據進行提取,每次都是對所有數據進行判斷,從而避免了鏈式結構的最大缺點。
我們使用鏈式存儲結構,雖然沒有降低問題的時間復雜度(鏈式存儲結構在最壞情況下的存儲量與順序存儲結構的存儲量幾乎相同),但由於體現了前文所述選擇存儲結構時揚長避短的原則,因而演算法的效率也大為提高。(程序對不同數據的運行時間見表3-3)
測試數據編號 使用順序存儲結構的程序 使用鏈式存儲結構的程序
1 0.06s 0.02s
2 1.73s 0.07s
3 1.14s 0.06s
4 3.86s 0.14s
5 32.84s 0.21s
6 141.16s 0.23s
7 0.91s 0.12s
8 6.92s 0.29s
9 6.10s 0.23s
10 17.41s 0.20s
表3-3
(附使用鏈式存儲結構的程序under.pas)
我們選擇鏈式的存儲結構,雖然操作上可能稍復雜一些,但由於改進了演算法的瓶頸,演算法的效率自然也今非昔比。由此可見,必要時選擇鏈式結構這一方法,其效果是不容忽視的。
小結
合理選擇邏輯結構,由於牽涉建立數學模型的問題,可能大家都會比較注意。但是對存儲結構的選擇,由於不會對演算法復雜度構成影響,所以比較容易忽視。那麼,這種不能降低演算法復雜度的方法是否需要重視呢?
大家都知道,剪枝作為一種常用的優化演算法的方法,被廣泛地使用,但剪枝同樣是無法改變演算法的復雜度的。因此,作用與剪枝相似的存儲結構的合理選擇,也是同樣很值得重視的。
總之,我們在設計演算法的過程中,必須充分考慮存儲結構所帶來的不同影響,選擇最合理的存儲結構。
四、 多種數據結構相結合
上文所探討的,都是如何對數據結構進行選擇,其中包含了邏輯結構的選擇和存儲結構的選擇,是一種具有較大普遍性的演算法優化方法。對於多數的問題,我們都可以通過選擇一種合理的邏輯結構和存儲結構以達到優化演算法的目的。
但是,有些問題卻往往不如人願,要對這類問題的數據結構進行選擇,常常會顧此失彼,有時甚至根本就不存在某一種合適的數據結構。此時,我們是無法選擇出某一種合適的數據結構的,以上的方法就有些不太適用了。
為解決數據結構難以選擇的問題,我們可以採用將多種數據結構進行結合的方法。通過多種數據結構相結合,達到取長補短的作用,使不同的數據結構在演算法中發揮出各自的優勢。
這只是我們將多種數據結構進行結合的總思想,具體如何進行結合,我們可以先看下面的例子。
我們可以採用映射的方法,將線性結構中的元素與堆中間的結點一一對應起來,若線性的數組中的元素發生變化,堆中相應的結點也接著變化,堆中的結點發生變化,數組中相應的元素也跟著變化。
將兩種結構進行結合後,無論是第一步還是第二步,我們都不需對所有元素進行遍歷,只需進行常數次復雜度為O(log2n)的堆化操作。這樣,整個時間復雜度就成為了O(nlog2n),演算法效率無疑得到了很大提高。
五、 總結
我們平常使用數據結構,往往只將其作為建立模型和演算法實現的工具,而沒有考慮這種工具對程序效率所產生的影響。信息學問題隨著難度的不斷增大,對演算法時空效率的要求也越來越高,而演算法的時空效率,在很大程度上都受到了數據結構的制約。
㈣ 調高控制面板里的滑鼠速度,還是調高滑鼠DPi好
當然是調DPI好。滑鼠的DPI是每英寸點數,也就是滑鼠每移動一英寸指針在屏幕上移動的點數。比如400DPI的滑鼠,他在移動一英寸的時候,屏幕上的指針可以移動400個點。這個是滑鼠精準定位的點。
系統控制面板的滑鼠速度是以插值演算法計算的,比如當控制面板把速度調快(標準的是最中間的檔位,就是6/11),那麼滑鼠移動實際只有1個點的距離,系統插值計算為兩個點距離,就是跳過一個點移動,這樣就移動更快,但是損失了精準的定位,調得越快定位越差(一倍是1個點當2個點,二倍就2個當4個,以此內推)。當速度調慢,把滑鼠真實移動兩個點移動的距離,只當成一個點距離,這樣移動就變慢了。
你可以理解成這樣,滑鼠DPI移動是真實的定位,比如你要移動800個點的距離,DPI是真實定位了這800點的每個點。控制面板滑鼠速度調快一倍,那麼它只定位了400個點,它是跳著兩個點當1個點移動的。反過來,控制面板調慢了,雖然你屏幕上的每一個點都能精準定位,但是卻犧牲了你滑鼠的定位。
即使你通過共同調節滑鼠和控制面板速度,搭配出來同樣的速度,在真實定位反應上都會有損失的。但是我們一般很多時候,並不需要滑鼠精確的定位每個點,但是像有些游戲或作圖之類的應用,對定位要求很高,這就需要對定位能力要求很高的滑鼠了(很多滑鼠本身晶元定位能力就差,或是晶元自身就是插值演算法的)。
㈤ 機械表的一些頂級工藝
陀飛輪是瑞士鍾表大師路易·寶璣先生在1795年發明的一種鍾表調速裝置。法文Tourbillon(故又稱特比龍),有「漩渦」之意,是指裝有「旋轉擒縱調速機構」的機械表,陀飛輪機構,是為了校正地心引力對鍾表機件造成的誤差。陀飛輪表代表了機械表製造工藝中的最高水平,整個擒縱調速機構組合在一起並且能夠轉動,以一定的速度不斷的旋轉,使其把地心引力對機械表中「擒縱系統」的影響減至最低程度,提高走時精度。由於其獨特的運行方式,已經把鍾表的動感藝術美發揮到登峰造極的地步,歷來被譽為「表中之王」。
陀飛輪的結構
陀飛輪的創意在於,將擒縱機構放在一個框架(Carriage)之內,使框架圍 陀飛輪
繞軸心也就是擺輪的軸心做360度不停的旋轉。這樣,原本的擒縱機構是固定的,因而當表擱置位置變化的時候,擒縱機構不變,造成了擒縱零件受力不同而產生了誤差;當擒縱機構360度不停的旋轉起來的時候,會將零件的方位誤差綜合起來,互相抵消,從而消滅誤差。目前陀飛輪一般是1分鍾轉360度,也是最理想的旋轉速度。
陀飛輪的原理就是當鍾表在垂直位置時補償地心引力的作用。 換句話說,當一隻鍾表處於垂直位置時,由於來自地心引力的作用,它的調節控制器,即是其擺輪、游絲和擒縱器,會在每一下擺動時發生難以覺察的快慢變化。 如果把調節控制器裝設在一個每分鍾轉動一周的「籠框」上,即可獲得一系列的垂直位置。這樣便可以使鍾表走動時十分准確,並能夠互補誤差。 這個原理看來十分簡單,但實施起來卻是另外一回事。原因之一便是「籠框」和陀飛輪的重量不能超過0.3克或0.013盎司——相當於一片天鵝羽毛的重量或兩片鸚鵡羽毛的重量。另一原因是,它由72個精細組件組成,而其中大部分為手工製作!
陀飛輪的技術演進
陀飛輪手錶經過時間和技術的改進和革新主要可以分為以下三代: 1、第一代陀飛輪表(即第一種結構—Tourbillon)是在1795年由瑞士製表大師Abraham-Louis Breguet發明並製成的。其飛輪結構必須由「飛輪旋轉框架」(Tourbillon』s Carriage)和「飛輪固定支架」(Tourbillon』s Bridge)不可或缺的兩部分基本構件組成。在此組合中,「擺輪夾板」(Balance』s Bridge)必須隨飛輪一起旋轉。按照不同的組合方式,第一代飛輪表可以分為兩類:同軸式(即擺輪的中心和飛輪的中心在同一軸心上);偏心式(亦稱非同軸式,即擺輪的中心和飛輪的中心不在同一軸心上)。 2、第二代飛行陀飛輪表(即第二種結構—Flying Tourbillon)是在1927年,德國製表大師Alferd Helwig製造成功沒有「飛輪固定支架」的陀飛輪懷表,提高了此種表運轉時的神秘感和動態藝術美。在此組合中「擺輪夾板」仍須隨飛輪一起旋轉,此第二代飛輪表同樣有同軸式和偏心式兩種類別。 3、第三代神奇陀飛輪表(即第三種結構—Mystery Tourbillon)則由東方的鍾表大師—中國人矯大羽(Kiu Tai Yu)在1993年於香港的「天儀軒」首創發明並且親手製造成功。它不但和第二代飛行陀飛輪表一樣都取消了「飛輪固定支架」,而且奇跡般地把「飛輪旋轉框架」也一同取消了(在第一代和第二代飛輪表中,此構件都是必不可少的)。另外,在這個全新的結構中還把第一代和第二代飛輪表中必須隨飛輪一起旋轉的「擺輪夾板」改變為不隨飛輪一起轉動,首次大大減輕了飛輪重量達一半以上,並且可以加大擺輪的直徑以增強計時的穩定性,同時又提高了動感藝術表現的水平。在飛輪表製造歷史中,矯大羽首次選用藍寶石玻璃替代了原本用金屬製造的「擺輪夾板」,之前此部件是附屬於「飛輪旋轉框架」中的。由於此表運轉時顯得更加神秘莫測,故在國際上也被稱為「矯氏神奇陀飛輪」(Kiu』s Mystery Tourbillon)和「中國陀飛輪」(Chinese Tourbillon)。
萬年歷
捲曲在鏈盤之內的發條是用手工裝上的。它緩緩地松開,通過齒輪拖動系統(由齒輪和小齒輪組成)把它的動力傳送到擒縱裝置上,(擒縱裝置調節它的力度)並傳送到游絲擺輪。游絲擺輪的功能是調節時間,它每秒鍾振動6到8次。振動越有規律,表的精確度就越高。齒輪拖動系統連接在游絲擺輪上,它的功能是驅動表盤上的指針。根據這個基本的原則(大約需要90個零部件),可以改製成一整套不同樣式、越來越復雜的機械表,可多達13種功能,包括萬年歷、月相等。一塊"頂級復雜"的機械表由1400個零部件組成。
普通的手錶是通過機械方式(機械表)或者簡單的換算方式(電子表)來實現日歷功能的。而「萬年歷」需要實現一個略為復雜的演算法,要實現這個演算法,普通的電子表控制晶元已經不能勝任,需要一塊8位以上的晶元來完成這個工作。所以,真正有萬年歷功能的手錶,基本是電子石英錶,機械表不能做到真正的自動有萬年歷功能,還要人工操作。
日月星辰就不太清楚了
㈥ 求一個C#中使用高性能數組(使用指針)的實際應用
stackalloc 創建一個高性能的數組
stackalloc命令指示.NET運行庫分配堆棧上一定量的內存。在調用它時,需要為它提供兩條信息:
● 要存儲的數據類型
● 需要存儲的數據個數。
例如,分配足夠的內存,以存儲10個decimal數據,可以編寫下面的代碼:
decimal* pDecimals = stackalloc decimal [10];
注意,這個命令只是分配堆棧內存而已。它不會試圖把內存初始化為任何默認值,這正好符合我們的目的。因為這是一個高性能的數組,給它不必要地初始化值會降低性能。
同樣,要存儲20個double數據,可以編寫下面的代碼:
double* pDoubles = stackalloc double [20];
雖然這行代碼指定把變數的個數存儲為一個常數,但它是在運行時計算的一個數字。所以可以把上面的示例寫為:
int size;
size = 20; // or some other value calculated at run-time
double* pDoubles = stackalloc double [size];
從這些代碼段中可以看出,stackalloc的語法有點不尋常。它的後面緊跟的是要存儲的數據類型名(該數據類型必須是一個值類型),其後是把需要的變數個數放在方括弧中。分配的位元組數是變數個數乘以sizeof(數據類型)。在這里,使用方括弧表示這是一個數組。如果給20個double數據分配存儲單元,就得到了一個有20個元素的double數組,最簡單的數組類型可以是:逐個存儲元素的內存塊,如圖7-6所示。
圖 7-6
在圖7-6中,顯示了一個由stackalloc返回的指針,stackalloc總是返回分配數據類型的指針,它指向新分配內存塊的頂部。要使用這個內存塊,可以取消對返回指針的引用。例如,給20個double數據分配內存後,把第一個元素(數組中的元素0)設置為3.0,可以編寫下面的代碼:
double* pDoubles = stackalloc double [20];
*pDoubles = 3.0;
要訪問數組的下一個元素,可以使用指針演算法。如前所述,如果給一個指針加1,它的值就會增加其數據類型的位元組數。在本例中,就會把指針指向下一個空閑存儲單元。因此可以把數組的第二個元素(數組中元素號為1)設置為8.4:
double* pDoubles = stackalloc double [20];
*pDoubles = 3.0;
*(pDoubles+1) = 8.4;
同樣,可以用表達式*(pDoubles+X)獲得數組中下標為X的元素。
這樣,就得到一種訪問數組中元素的方式,但對於一般目的,使用這種語法過於復雜。C#為此定義了另一種語法。對指針應用方括弧時,C#為方括弧提供了一種非常明確的含義。如果變數p是任意指針類型,X是一個整數,表達式p[X]就被編譯器解釋為*(p+X),這適用於所有的指針,不僅僅是用stackalloc初始化的指針。利用這個簡捷的記號,就可以用一種非常方便的方式訪問數組。實際上,訪問基於堆棧的一維數組所使用的語法與訪問基於堆的、由System.Array類表示的數組是一樣的:
double *pDoubles = stackalloc double [20];
pDoubles[0] = 3.0; // pDoubles[0] is the same as *pDoubles
pDoubles[1] = 8.4; // pDoubles[1] is the same as *(pDoubles+1)
注意:
把數組的語法應用於指針並不是新東西。自從開發出C和C++語言以來,它們就是這兩種語言的基礎部分。實際上,C++開發人員會把這里用stackalloc獲得的、基於堆棧的數組完全等同於傳統的基於堆棧的C和C++數組。這個語法和指針與數組的鏈接方式是C語言在70年代後期流行起來的原因之一,也是指針的使用成為C和C++中一種大眾化編程技巧的主要原因。
高性能的數組可以用與一般C#數組相同的方式訪問,但需要強調其中的一個警告。在C#中,下面的代碼會拋出一個異常:
double [] myDoubleArray = new double [20];
myDoubleArray[50] = 3.0;
拋出異常的原因很明顯。使用越界的下標來訪問數組:下標是50,但允許的最大值是19。但是,如果使用stackalloc聲明了一個相同數組,對數組進行邊界檢查時,這個數組中沒有包裝任何對象,因此下面的代碼不會拋出異常:
double* pDoubles = stackalloc double [20];
pDoubles[50] = 3.0;
在這段代碼中,我們分配了足夠的內存來存儲20個double類型數據。接著把sizeof(double)存儲單元的起始位置設置為該存儲單元的起始位置加上50*sizeof(double)存儲單元,來保存雙精度值3.0。但這個存儲單元超出了剛才為double分配的內存區域。誰也不知道這個地址上存儲了什麼數據。最好是只使用某個當前未使用的內存,但所重寫的空間也有可能是堆棧上用於存儲其他變數或某個正在執行的方法的返回地址。因此,使用指針獲得高性能的同時,也會付出一些代價:需要確保自己知道在做什麼,否則就會拋出非常古怪的運行時錯誤。
2. 示例QuickArray
下面用一個stackalloc示例QuickArray來結束關於指針的討論。在這個示例中,程序僅要求用戶提供為數組分配的元素數。然後代碼使用stackalloc給long型數組分配一定的存儲單元。這個數組的元素是從0開始的整數的平方,結果顯示在控制台上:
using System;
namespace Wrox.ProCSharp.Chapter07
{
class MainEntryPoint
{
static unsafe void Main()
{
Console.Write("How big an array do you want? \n> ");
string userInput = Console.ReadLine();
uint size = uint.Parse(userInput);
long* pArray = stackalloc long [(int)size];
for (int i=0 ; i pArray = i*i;
for (int i=0 ; i Console.WriteLine("Element {0} = {1}", i, *(pArray+i));
}
}
}
運行這個示例,得到如下所示的結果:
QuickArray
How big an array do you want?
> 15
Element 0 = 0
Element 1 = 1
Element 2 = 4
Element 3 = 9
Element 4 = 16
Element 5 = 25
Element 6 = 36
Element 7 = 49
Element 8 = 64
Element 9 = 81
Element 10 = 100
Element 11 = 121
Element 12 = 144
Element 13 = 169
Element 14 = 196
7.4 小結
要想成為真正優秀的C#程序員,必須牢固掌握存儲單元和垃圾收集的工作原理。本文描述了CLR管理以及在堆和堆棧上分配內存的方式,討論了如何編寫正確釋放未託管資源的類,並介紹如何在C#中使用指針,這些都是很難理解的高級主題,初學者常常不能正確實現。
本文摘至清華大學出版社出版的Wrox紅皮書《C#高級編程(第3版)》,
㈦ 什麼是提高指針精確度.
提高指針精確度就是滑鼠加速度
指針是對圖案視覺的設置,指針選項則是對操作方面的設置,拖動「選擇指針移動速度」上的滑塊,就能調節滑鼠指針在屏幕上移動的速度。建議在「提高指針精確度」旁邊的方框打上×號,這樣有利於滑鼠更精確地定位。
㈧ 什麼是高精度
高精度演算法在一般的科學計算中,會經常算到小數點後幾百位或者更多,當然也可能是幾千億幾百億的大數字.
一般這類數字我們統稱為高精度數,高精度演算法是用計算機對於超大數據的一種模擬加,減,乘,除,乘方,階乘,開方等運算.
譬如一個很大的數字N >= 10^ 100, 很顯然這樣的數字無法在計算機中正常存儲.
於是, 我們想到了辦法,將這個數字拆開,拆成一位一位的 或者是四位四位的存儲到一個數組中, 用一個數組去表示一個數字.這樣這個數字就被稱謂是高精度數.
對於高精度數,也要像平常數一樣做加減乘除以及乘方的運算,於是就有了高精度演算法:
下面提供了Pascal的高精度加法, 高精度乘以單精度, 高精度乘以高精度的代碼, 其他版本請各位大牛添加進來吧!
Pascal代碼如下(非完整); k為預定進制,加大進制以提高速度。
Procere HPule(a, b: Arr; Var c:Arr); //高精度加法
Var
i: Integer;
Begin
FillChar(c, SizeOf(c), 0);
For i:= 1 To Maxn-1 Do Begin
c[i]:= c[i] + a[i] + b[i];
c[i + 1] := c[i] Div k;
c[i] := c[i] Mod k;
End;
End;
Procere HPule(a: Arr; b:Integer; Var c:Arr); //高精度乘以單精度
Var
i: Integer;
Begin
FillChar(c, SizeOf(c), 0);
For i:= 1 To Maxn-1 Do Begin
c[i] := c[i] + a[i] * b;
c[i+1]:= c[i] Div k;
c[i]:= c[i] Mod k
End;
End;
Procere HPule(a, b: Arr; ; Var c:Arr); //高精度乘以高精度
Var
i, j: Integer;
Begin
FillChar(c, SizeOf(c), 0);
For i:= 1 To Maxn Do
For j := 1 To Maxn Begin
c[i+j-1] := c[i+j-1] + a[i] * b[j];
c[i+j]:= c[i+j-1] Div k;
c[i+j-1]:= c[i+j-1] Mod k
End;
End;
Ps:為了防止網路錯誤識別, 過程中有不少符號是全形狀態輸入.
高精度加法
var
a,b,c:array[1..201] of 0..9;
n:string;
lena,lenb,lenc,i,x:integer;
begin
write('Input augend:'); readln(n);lena:=length(n);
for i:=1 to lena do a[lena-i+1]:=ord(n)-ord('0');{加數放入a數組}
write('Input addend:'); readln(n); lenb:=length(n);
for i:=1 to lenb do b[lenb-i+1]:=ord(n)-ord('0');{被加數放入b數組}
i:=1;
while (i<=lena) or(i<=lenb) do
begin
x := a + b + x div 10; {兩數相加,然後加前次進位}
c := x mod 10; {保存第i位的值}
i := i + 1
end;
if x>=10 {處理最高進位}
then begin lenc:=i; c:=1 end
else lenc:=i-1;
for i:=lenc downto 1 do write(c); writeln {輸出結果}
end.
高精度乘法(低對高)
const max=100; n=20;
var a:array[1..max]of 0..9;
i,j,k;x:integer;
begin
k:=1; a[k]:=1;{a=1}
for i:=2 to n do{a*2*3….*n}
begin
x:=0;{進位初始化}
for j:=1 do k do{a=a*i}
begin
x:=x+a[j]*i; a[j]:=x mod 10;x:=x div 10
end;
while x>0 do {處理最高位的進位}
begin
k:=k+1;a[k]:=x mod 10;x:=x div 10
end
end;
writeln;
for i:=k dowento 1 write(a){輸出a}
end.
高精度乘法(高對高)
var a,b,c:array[1..200] of 0..9;
n1,n2:string; lena,lenb,lenc,i,j,x:integer;
begin
write('Input multiplier:'); readln(n1);
write('Input multiplicand:'); readln(n2);
lena:=length(n1); lenb:=length(n2);
for i:=1 to lena do a[lena-i+1]:=ord(n1)-ord('0');
for i:=1 to lenb do b[lenb-i+1]:=ord(n2)-ord('0');
for i:=1 to lena do
begin
x:=0;
for j:=1 to lenb do{對乘數的每一位進行處理}
begin
x := a*b[j]+x div 10+c;{當前乘積+上次乘積進位+原數}
c:=x mod 10;
end;
c:= x div 10;{進位}
end;
lenc:=i+j;
while (c[lenc]=0) and (lenc>1) do dec(lenc); {最高位的0不輸出}
for i:=lenc downto 1 do write(c); writeln
end.
高精度除法
fillchar(s,sizeof(s),0);{小數部分初始化}
fillchar(posi,sizeof(posi),0); {小數值的位序列初始化}
len←0;st←0; {小數部分的指針和循環節的首指針初始化}
read(x,y);{讀被除數和除數}
write(x div y);{輸出整數部分}
x←x mod y;{計算x除以y的余數}
if x=0 then exit;{若x除盡y,則成功退出}
while len<limit do{若小數位未達到上限,則循環}
begin
inc(len);posi[x]←len;{記下當前位小數,計算下一位小數和余數}
x←x*10; s[len]←x div y;x←x mod y;
if posi[x]<>0 {若下一位余數先前出現過,則先前出現的位置為循環節的開始}
then begin st←posi[x]; break;end;{then}
if x=0 then break; {若除盡,則成功退出}
end;{while}
if len=0
then begin writeln;exit;end;{若小數部分的位數為0,則成功退出;否則輸出小數點}
write('.');
if st=0 {若無循環節,則輸出小數部分,否則輸出循環節前的小數和循環節}
then for i←1 to len do write(s)
else begin
for i←1 to st-1 do write(s);
write('(');
for i←st to len do write(s);
write(')');
end;{else}
㈨ C++指針問題
所不同的是一般的變數包含的是實際的真實的數據,而指針是一個指示器,它告訴程序在內存的哪塊區域可以找到數據。這是一個非常重要的概念,有很多程序和演算法都是圍繞指針而設計的,如鏈表。
開始學習
如何定義一個指針呢?就像你定義一個其它變數一樣,只不過你要在指針名字前加上一個星號。我們來看一個例子:
下面這個程序定義了兩個指針,它們都是指向整型數據。
int* pNumberOne;
int* pNumberTwo;
你注意到在兩個變數名前的「p」前綴了嗎?這是程序員通常在定義指針時的一個習慣,以提高便程序的閱讀性,表示這是個指針。現在讓我們來初始化這兩個指針:
pNumberOne = &some_number;
pNumberTwo = &some_other_number;
&號讀作「什麼的地址」,它表示返回的是變數在內存中的地址而不是變數本身的值。在這個例子中,pNumberOne 等於
some_number的地址,所以現在pNumberOne指向some_number。 如果現在我們在程序中要用到some_number,我們就
可以使用pNumberOne。
我們來學習一個例子:
在這個例子中你將學到很多,如果你對指針的概念一點都不了解,我建議你多看幾遍這個例子,指針是個很復雜的東西,但你會很快掌握它的。
這個例子用以增強你對上面所介紹內容的了解。它是用C編寫的(註:原英文版是用C寫的代碼,譯者重新用C++改寫寫了所有代碼,並在DEV C++ 和VC++中編譯通過!)
#include
void main()
{
// 聲明變數:
int nNumber;
int *pPointer;
// 現在給它們賦值:
nNumber = 15;
pPointer = &nNumber;
//列印出變數nNumber的值:
cout"nNumber is equal to :";
// 現在通過指針改變nNumber的值:
*pPointer = 25;
//證明nNumber已經被上面的程序改變
//重新列印出nNumber的值:
cout"nNumber is equal to :";
}
通讀一下這個程序,編譯並運行它,務必明白它是怎樣工作的。如果你完成了,准備好,開始下一小節。
陷井!
試一下,你能找出下面這段程序的錯誤嗎?
#include
int *pPointer;
void SomeFunction();
{
int nNumber;
nNumber = 25;
//讓指針指向nNumber:
pPointer = &nNumber;
}
void main()
{
SomeFunction(); //為pPointer賦值
//為什麼這里失敗了?為什麼沒有得到25
cout"Value of *pPointer: ";
}
這段程序先調用了SomeFunction函數,創建了個叫nNumber的變數,接著讓指針pPointer指向了它。可是問題出在哪兒呢?當函數結
束後,nNumber被刪掉了,因為這一個局部變數。局部變數在定義它的函數執行完後都會被系統自動刪掉。也就是說當SomeFunction 函數返回
主函數main()時,這個變數已經被刪掉,但pPointer還指著變數曾經用過的但現在已不屬於這個程序的區域。如果你還不明白,你可以再讀讀這個程
序,注意它的局部變數和全局變數,這些概念都非常重要。
但這個問題怎麼解決呢?答案是動態分配技術。注意這在C和C++中是不同的。由於大多數程序員都是用C++,所以我用到的是C++中常用的稱謂。
動態分配
動態分配是指針的關鍵技術。它是用來在不必定義變數的情況下分配內存和讓指針去指向它們。盡管這么說可能會讓你迷惑,其實它真的很簡單。下面的代碼就是一個為一個整型數據分配內存的例子:
int *pNumber;
pNumber = new int;
第一行聲明一個指針pNumber。第二行為一個整型數據分配一個內存空間,並讓pNumber指向這個新內存空間。下面是一個新例,這一次是用double雙精型:
double *pDouble;
pDouble = new double;
這種格式是一個規則,這樣寫你是不會錯的。
但動態分配又和前面的例子有什麼不同呢?就是在函數返回或執行完畢時,你分配的這塊內存區域是不會被刪除的所以我們現在可以用動態分配重寫上面的程序:
#include
int *pPointer;
void SomeFunction()
{
// 讓指針指向一個新的整型
pPointer = new int;
*pPointer = 25;
}
void main()
{
SomeFunction(); // 為pPointer賦值
cout"Value of *pPointer: ";
}
通讀這個程序,編譯並運行它,務必理解它是怎樣工作的。當SomeFunction 調用時,它分配了一個內存,並讓pPointer指向它。這一次,
當函數返回時,新的內存區域被保留下來,所以pPointer始終指著有用的信息,這是因為了動態分配。但是你再仔細讀讀上面這個程序,雖然它得到了正確
結果,可仍有一個嚴重的錯誤。
分配了內存,別忘了回收
太復雜了,怎麼會還有嚴重的錯誤!其實要改正並不難。問題是:你動態地分配了一
個內存空間,可它絕不會被自動刪除。也就是說,這塊內存空間會一直存在,直到你告訴電腦你已經使用完了。可結果是,你並沒有告訴電腦你已不再需要這塊內存
空間了,所以它會繼續占據著內存空間造成浪費,甚至你的程序運行完畢,其它程序運行時它還存在。當這樣的問題積累到一定程度,最終將導致系統崩潰。所以這
是很重要的,在你用完它以後,請釋放它的空間,如:
delete pPointer;
這樣就差不多了,你不得不小心。在這你終止了一個有效的指針(一個確實指向某個內存的指針)。
下面的程序,它不會浪費任何的內存:
#include
int *pPointer;
void SomeFunction()
{
// 讓指針指向一個新的整型
pPointer = new int;
*pPointer = 25;
}
void main()
{
SomeFunction(); //為pPointer賦值
cout"Value of *pPointer: ";
delete pPointer;
}
只有一行與前一個程序不同,但就是這最後一行十分地重要。如果你不刪除它,你就會製造一起「內存漏洞」,而讓內存逐漸地泄漏。
(譯者:假如在程序中調用了兩次SomeFunction,你又該如何修改這個程序呢?請讀者自己思考)
傳遞指針到函數
傳遞指針到函數是非常有用的,也很容易掌握。如果我們寫一個程序,讓一個數加上5,看一看這個程序完整嗎?:
#include
void AddFive(int Number)
{
Number = Number + 5;
}
void main()
{
int nMyNumber = 18;
cout"My original number is ";
AddFive(nMyNumber);
cout"My new number is ";
//得到了結果23嗎?問題出在哪兒?
}
問題出在函數AddFive里用到的Number是變數nMyNumber的一個副本而傳遞給函數,而不是變數本身。因此, " Number =
Number + 5" 這一行是把變數的副本加了5,而原始的變數在主函數main()里依然沒變。試著運行這個程序,自己去體會一下。
要解決這個問題,我們就要傳遞一個指針到函數,所以我們要修改一下函數讓它能接受指針:把'void AddFive(int Number)' 改成
'void AddFive(int* Number)' 。下面就是改過的程序,注意函數調用時要用&號,以表示傳遞的是指針:
#include
void AddFive(int* Number)
{
*Number = *Number + 5;
}
void main()
{
int nMyNumber = 18;
cout"My original number is ";
AddFive(&nMyNumber);
cout"My new number is ";
}
試著自己去運行它,注意在函數AddFive的參數Number前加*號的重要性:它告訴編譯器,我們是把指針所指的變數加5。而不並指針自己加5。
最後,如果想讓函數返回指針的話,你可以這么寫:
int * MyFunction();
在這句里,MyFunction返回一個指向整型的指針。
指向類的指針
指針在類中的操作要格外小心,你可以用如下的辦法定義一個類:
class MyClass
{
public:
int m_Number;
char m_Character;
};
接著你就可以定義一個MyClass 類的變數了:
MyClass thing;
你應該已經知道怎樣去定義一個指針了吧:
MyClass *thing;
接著你可以分配個內存空間給它:
thing = new MyClass;
注意,問題出現了。你打算怎樣使用這個指針呢,通常你可能會寫'thing.m_Number',但是thing是類嗎,不,它是一個指向類的指針,它
本身並不包含一個叫m_Number的變數。所以我們必須用另一種方法:就是把'.'(點號)換成 -> ,來看下面的例子:
class MyClass
{
public:
int m_Number;
char m_Character;
};
void main()
{
MyClass *pPointer;
pPointer = new MyClass;
pPointer->m_Number = 10;
pPointer->m_Character = 's';
delete pPointer;
}
指向數組的指針
你也可以讓指針指向一個數組,按下面的方法操作:
int *pArray;
pArray = new int[6];
程序會創建一個指針pArray,讓它指向一個有六個元素的數組。另外一種方法,不用動態分配:
int *pArray;
int MyArray[6];
pArray = &MyArray[0];
注意,&MyArray[0] 也可以簡寫成 MyArray ,都表示是數組的第一個元素地址。但如果寫成pArray = &
MyArray可能就會出問題,結果是 pArray 指向的是指向數組的指針(在一維數組中盡管與&MyArray[0]相等),而不是你想要
的,在多維數組中很容易出錯。
在數組中使用指針
一旦你定義了一個指向數組的指針,你該怎樣使用它呢?讓我們來看一個例子,一個指向整型數組的指針:
#include
void main()
{
int Array[3];
Array[0] = 10;
Array[1] = 20;
Array[2] = 30;
int *pArray;
pArray = &Array[0];
cout"pArray points to the value %d\n";
}
如果讓指針指向數組元素中的下一個,可以用pArray++.也可以用你應該能想到的pArray + 1,都會讓指針指向數組的下一個元素。要注意的
是你在移動指針時,程序並不檢查你是否已經移動地超出了你定義的數組,也就是說你很可能通過上面的簡單指針加操作而訪問到數組以外的數據,而結果就是,可
能會使系統崩潰,所以請格外小心。
當然有了pArray + 1,也可以有pArray - 1,這種操作在循環中很常用,特別是while循環中。
另一個需要注意的是,如果你定義了一個指向整型數的指針:int* pNumberSet ,你可以把它當作是數組,如:pNumberSet[0] 和 *pNumberSet是相等的,pNumberSet[1]與*(pNumberSet + 1)也是相等的。
在這一節的最後提一個警告:如果你用 new 動態地分配了一個數組,
int *pArray;
pArray = new int[6];
別忘了回收,
delete[] pArray;
這一句是告訴編譯器是刪除整個數組而不一個單獨的元素。千萬記住了。
後話
還有一點要小心,別刪除一個根本就沒分配內存的指針,典型的是如果沒用new分配,就別用delete:
void main()
{
int number;
int *pNumber = number;
delete pNumber; // 錯誤 - *pNumber 沒有用new動態分配內存.
}
常見問題解答
Q:為什麼我在編譯程序時老是在 new 和 delete語句中出現'symbol undefined' 錯誤?
A:new 和 delete都是C++在C上的擴展,這個錯誤是說編譯器認為你現在的程序是C而不C++,當然會出錯了。看看你的文件名是不是.cpp結尾。
Q:new 和 malloc有什麼不同?
A:new 是C++中的關健字,用來分配內存的一個標准函數。如果沒有必要,請不要在C++中使用malloc。因為malloc是C中的語法,它不是為面向對象的C++而設計的。
Q:我可以同時使用free 和 delete嗎?
A:你應該注意的是,它們各自所匹配的操作不同。free只用在用malloc分配的內存操作中,而delete只用在用new分配的內存操作中。
引用(寫給某些有能力的讀者)
這一節的內容不是我的這篇文章的中心,只是供某些有能力的讀者參考。
有些讀者經常問我關於引用和指針的問題,這里我簡要地討論一下。
在前面指針的學習中,我們知道(&)是讀作「什麼的地址」,但在下面的程序中,它是讀作「什麼的引用」
int& Number = myOtherNumber;
Number = 25;
引用有點像是一個指向myOtherNumber的指針,不同的是它是自動刪除的。所以他比指針在某些場合更有用。與上面等價的代碼是:
int* pNumber = &myOtherNumber;
*pNumber = 25;
指針與引用另一個不同是你不能修改你已經定義好的引用,也就是說你不能改變它在聲明時所指的內容。舉個例子:
int myFirstNumber = 25;
int mySecondNumber = 20;
int &myReference = myFirstNumber;
myReference = mySecondNumber;//這一步能使myReference 改變嗎?
cout<<myFristNumber<<endl;//結果是20還是25?
當在類中操作時,引用的值必須在構造函數中設定,例:
CMyClass::CMyClass(int &variable) : m_MyReferenceInCMyClass(variable)
{
// constructor code here
}
總結
這篇文章開始可能會較難掌握,所以最好是多讀幾遍。有些讀者暫時還不能理解,在這兒我再做一個簡要的總結:
指針是一個指向內存區域的變數,定義時在變數名前加上星號(*)(如:int *number)。
你可以得到任何一個變數的地址,只在變數名前加上&(如:pNumber = &my_number)。
你可以用'new' 關鍵字動態分配內存。指針的類型必須與它所指的變數類型一樣(如:int *number 就不能指向 MyClass)。
你可以傳遞一個指針到函數。必須用'delete'刪除你動態分配的內存。
你可以用&array[0]而讓指針指向一個數組。
你必須用delete[]而不是delete來刪除動態分配的數組。
文章到這兒就差不多結束了,但這些並不就是指針所有的東西,像指向指針的指針等我還沒有介紹,因為這些東西對於一個初學指針的人來說還太復雜了,我不能
讓讀者一開始就被太復雜的東西而嚇走了。好了,到這兒吧,試著運行我上面寫的小程序,也多自己寫寫程序,你肯定會進步不小的!