A. 計算機演算法必須具備哪5個特性
1、有窮性。一個演算法應包含有限的操作步驟,而不能是無限的。事實上「有窮性」往往指「在合理的范圍之內」。如果讓計算機執行一個歷時1000年才結束的演算法,這雖然是有窮的,但超過了合理的限度,人們不把他視為有效演算法。
2、確定性。演算法中的每一個步驟都應當是確定的,而不應當是含糊的、模稜兩可的。演算法中的每一個步驟應當不致被解釋成不同的含義,而應是十分明確的。也就是說,演算法的含義應當是唯一的,而不應當產生「歧義性」。
3、有零個或多個輸入性。所謂輸入是指在執行演算法是需要從外界取得必要的信息。
4、有一個或多個輸出。演算法的目的是為了求解,沒有輸出的演算法是沒有意義的。
5、有效性。 演算法中的每一個 步驟都應當能有效的執行。並得到確定的結果。
(1)演算法之道第2版擴展閱讀
計算機演算法的產生背景:
歐幾里得演算法被人們認為是史上第一個演算法。 第一次編寫程序是Ada Byron於1842年為巴貝奇分析機編寫求解伯努利方程的程序,因此Ada Byron被大多數人認為是世界上第一位程序員。
因為查爾斯·巴貝奇未能完成他的巴貝奇分析機,這個演算法未能在巴貝奇分析機上執行。 因為"well-defined procere"缺少數學上精確的定義,19世紀和20世紀早期的數學家、邏輯學家在定義演算法上出現了困難。
B. (轉)車輛路徑問題及行業應用
轉自:吉勍Personal http://www.jiqingip.com/page9001?article_id=96
車輛路徑問題是運行日常操作所需的操作決策的一部分,都是執行層面的優化問題。先決條件如下:通常情況下,已知資源不能在短時間內擴充,因此資源稀缺是無法避免的。此外,還提供了關於需要滿足的需求的詳細信息。決策優化的核心挑戰是在日常基礎上使需求與現有資源相匹配。在這里,必須解決兩個基本的決策任務:(1)必須給需求分配資源,(2)必須制定日程安排。日程安排描述了執行分配任務的順序,以及啟動單個操作的起點。最大的挑戰是確保不超過現有資源,並以最高效率部署這些資源。
問題描述
車輛路徑問題(VRP)是一個組合優化和整數規劃問題(解決的是「為了交付給定的一組客戶,車輛車隊的最佳路線集是什麼?」)。它概括了眾所周知的旅行推銷員問題(TSP)。它最初出現在1959年George Dantzig和John Ramser的論文中《The Truck Dispatching Problem》。這篇論文首先編寫了演算法,並將其應用於汽油交付。通常,這個問題的背景是將位於中央倉庫的貨物交付給已經訂購此類貨物的客戶。該問題的目標是最小化總路由成本。車輛路徑規劃問題在物流領域和生產散枝橘領域的應用非常廣泛。所以在實際應用中也出現了一些在標准問題的基礎上增加了某些變化之後的變型問題。其中較為常見的包括:
1. CVRP:Capacitated
VRP, 限制配送車輛的承載體積、重量等。
2. VRPTW:VRP with
Time Windows, 客戶對貨物的送達時間有時間窗要求。
3. VRPPD:VRP with
Pickup and Delivery, 車輛在配送過程中可以一邊攬收一邊配送,在外賣O2O場景中比較常見。
4. MDVRP: Multi-Depot
VRP, 配送網路中有多個倉庫,同樣的貨物可以在多個倉庫取貨。
5. OVRP:Open VRP, 車輛完成配送任務之後不需要返回倉庫。
6. VRPB: VRP with
backhauls, 車輛完成配送任務之後回程取貨。
車輛路徑問題
模型描述
TSP問題
TSP問題模型的基本思想是,節點集中包含的每個弧(i; j)要麼包含在漢密爾頓路徑中(通過所有N個節點的往返行程),要麼不包含。在提到的第一種情況下,節點j在節點i之後立即被訪問,但是在後一種搭辯情況下,節點j在i離開之後不立即被訪問。 TSP決策問題可以簡化為以下問題:哪些弧形成了請求的哈密頓沖團路徑,哪些弧被忽略了。為了表示這些二元決策,引入了二元決策變數xij(i∈{1,...,N}系列。每個決策變數xij為0或1,xij表示是否為arc(i ; j)是否包含在漢密爾頓路徑中,並且僅當在漢密爾頓路徑中包含arc(i; j)時,才將xij聲明為1;如果不成立,則等於0。d(i,j)表示節點i和節點j之間的距離。
優化目標使所有在哈密頓路徑中的所有弧的行程距離之和最小。約束保證了漢密爾頓迴路經過所有節點,且每個節點只經過一次。後兩條約束保證了漢密爾頓迴路是連續而非中斷的。
VRP問題
標準的車輛路徑規劃問題可以使用如下數據模型的形式描述:
在此公式中,(1),(2),(3)和(5)定義了一個修改的分配問題,約束(4)是子行程消除約束:v(S)是在最佳解決方案中訪問S的所有頂點所需的車輛數量的適當下限。其他變型VRP問題則可以在此模型基礎上做適當的調整。
演算法服務
有很多實際的業務場景,即時配、大件配送、冷鏈配送、門店補貨等,都可以通過VRP問題優化其配送成本。這些業務場景屬於不同的業態,所使用的業務系統也不盡相同,因此構建可靈活配置的VRP演算法服務平台,可達成一次構建,多業務系統調用,多場景應用的效果。
行業應用
克里斯蒂娜在RED SEA BUS
TRAVEL(RSBT)工作。該公司在洪加達地區提供運輸服務。國際旅行社預訂RSBT服務業務,以確保將他們的遊客轉移到他們偏愛的度假區。克里斯蒂娜(Christina)被分配到洪加達市中心的RSBT計劃和調度辦公室。經過數年的復雜經營,RSBT發現來自旅行社的預訂量不斷增加,但來自個人客戶的預訂量卻不斷增加。這些預訂可以分為以下四個類別:
1) 豪華轎車服務(LS)
2) 觀光游覽(SST)
3) 機場到達接送(AAT)
4) 機場出發接送(ADT)
Christina現在的任務是分析LS,SST,AAT和ADT這四種產品,並就如何進行可用巴士(它們是可用資源)的日常部署提出建議。在滿足所有預訂要求的同時,以最有效的方式使用這些資源。克里斯蒂娜(Christina)在RSBT的第一周就曾陪同過幾項運輸服務,她發現了四個業務領域的核心規劃挑戰:
LS:通過當地道路網路從豪華轎車服務總部到機場的最短(最快)路徑是什麼?如何確定此路徑?
SST:應該以什麼順序參觀所有的旅遊景點,以便遊客有足夠的時間在酒店享受休閑時光?
AAT:將與入境航班相關的所有入境旅客帶到酒店的最小旅行距離是多少?最少需要多少輛巴士?
ADT:如果客戶在預定航班起飛前不超過5小時不接受接送服務,那輛巴士應該接送哪家酒店的客人以准時送他們到機場?
通過分析發現,克里斯蒂娜(Christina)需要解決的問題通過VRP演算法平台可以有效的給出計劃和調度方案。
首先從業務生產系統錄入相關信息,這些信息經過數據資產管理處理後,將數據傳給VRP演算法中台,經演算法中台處理後再返回給業務生產系統,生成業務系統的業務數據給業務系統使用。
C. 演算法效率與分析
演算法效率與分析
數據結構作為程序設計的基礎,其對演算法效率的影響必然是不可忽視的。本文就如何合理選擇數據結構來優化演算法這一問題,對選擇數據結構的原則和方法進行了一些探討。首先對數據邏輯結構的重要性進行了分析,提出了選擇邏輯結構的兩個基本原則;接著又比較了順序和鏈式兩種存儲結構的優點和缺點,並討論了選擇數據存儲結構的方法;最後本文從選擇數據結構的的另一角度出發,進一步探討了如何將多種數據結構進行結合的方法。在討論方法的同時,本文還結合實際,選用了一些較具有代表性的信息學競賽試題舉例進行了分析
【正文】一、引論
「數據結構+演算法=程序」,這就說明程序設計的實質就是對確定的問題選擇一種合適的數據結構,加上設計一種好的演算法。由此可見,數據結構在程序設計中有著十分重要的地位。
數據結構是相互之間存在一種或多種特定關系的數據元素的集合。因為這其中的「關系」,指的是數據元素之間的邏輯關系,因此數據結構又稱為數據的邏輯結構。而相對於邏輯結構這個比較抽象的概念,我們將數據結構在計算機中的表示又稱為數據的存儲結構。
建立問題的數學模型,進而設計問題的演算法,直至編出程序並進行調試通過,這就是我們解決信息學問題的一般步驟。我們要建立問題的數學模型,必須首先找出問題中各對象之間的關系,也就是確定所使用的邏輯結構;同時,設計演算法和程序實現的過程,必須確定如何實現對各個對象的操作,而操作的方法是決定於數據所採用的存儲結構的。因此,數據邏輯結構和存儲結構的好壞,將直接影響到程序的效率。
二、選擇合理的邏輯結構
在程序設計中,邏輯結構的選用就是要分析題目中的數據元素之間的關系,並根據這些特定關系來選用合適的邏輯結構以實現對問題的數學描述,進一步解決問題。邏輯結構實際上是用數學的方法來描述問題中所涉及的操作對象及對象之間的關系,將操作對象抽象為數學元素,將對象之間的復雜關系用數學語言描述出來。
根據數據元素之間關系的不同特性,通常有以下四種基本邏輯結構:集合、線性結構、樹形結構、圖狀(網狀)結構。這四種結構中,除了集合中的數據元素之間只有「同屬於一個集合」的關系外,其它三種結構數據元素之間分別為「一對一」、「一對多」、「多對多」的關系。
因此,在選擇邏輯結構之前,我們應首先把題目中的操作對象和對象之間的關系分析清楚,然後再根據這些關系的特點來合理的選用邏輯結構。尤其是在某些復雜的問題中,數據之間的關系相當復雜,且選用不同邏輯結構都可以解決這一問題,但選用不同邏輯結構實現的演算法效率大不一樣。
對於這一類問題,我們應採用怎樣的標准對邏輯結構進行選擇呢?
下文將探討選擇合理邏輯結構應充分考慮的兩個因素。
一、 充分利用「可直接使用」的信息。
首先,我們這里所講的「信息」,指的是元素與元素之間的關系。
對於待處理的信息,大致可分為「可直接使用」和「不可直接使用」兩類。對於「可直接使用」的信息,我們使用時十分方便,只需直接拿來就可以了。而對於「不可直接使用」的這一類,我們也可以通過某些間接的方式,使之成為可以使用的信息,但其中轉化的過程顯然是比較浪費時間的。
由此可見,我們所需要的是盡量多的「可直接使用」的信息。這樣的信息越多,演算法的效率就會越高。
對於不同的邏輯結構,其包含的信息是不同的,演算法對信息的利用也會出現不同的復雜程度。因此,要使演算法能夠充分利用「可直接使用」的信息,而避免演算法在信息由「不可直接使用」向「可直接使用」的轉化過程中浪費過多的時間,我們必然需要採用一種合理的邏輯結構,使其包含更多「可直接使用」的信息。
〖問題一〗 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),演算法效率無疑得到了很大提高。
五、 總結
我們平常使用數據結構,往往只將其作為建立模型和演算法實現的工具,而沒有考慮這種工具對程序效率所產生的影響。信息學問題隨著難度的不斷增大,對演算法時空效率的要求也越來越高,而演算法的時空效率,在很大程度上都受到了數據結構的制約。
D. 急求完善兩道簡單的演算法分析題
第一題的題目有誤吧
返回小於x的最大元素的位置I和大於x的最大元素位置j
應該是
返回小於x的最大元素的位置I和大於x的最小元素位置j
演算法有點問題
第二題的演算法不對
兩道題目都是要假設 a[0:n-1]是一個已排好序的數組 並且是嚴格單調遞增的。
// 二分查找.cpp : 定義控制台應用程序的入口點。
//
#include "stdafx.h"
#include <stdio.h>
#define N 10
bool BinarySearch(int a[],int n,int x,int& i,int& j)
{
int left=0;
int right=n-1;
while(left<right-1)
{
int mid=(left+right)/2;
if(x==a[mid])
{
i=j=mid;
return true;
}
if(x>a[mid])
left=mid;
else
right=mid;
}
i=right;
j=left;
return false;
}
int SearchTag(int a[],int n)
{
int left=0;
int right=n-1;
while(left<right-1)
{
int mid=(left+right)/2;
if(mid==a[mid]) return mid;
if(mid<a[mid])
right=mid;
else
left=mid;
}
return -1;
}
int _tmain(int argc, _TCHAR* argv[])
{
int a[N];
printf("請輸入%d個由小到大排列的整數\n以空格隔開,以回車結束\n",N);
int k;
for(k=0;k<N;k++)
scanf("%d",a+k);
printf("你的輸入如下:\n");
for(k=0;k<N;k++)
printf("%d\t",k);
//printf("\n");
for(k=0;k<N;k++)
printf("%d\t",a[k]);
printf("\n");
if((k=SearchTag(a,N))!=-1)
printf("第%d個數字滿足a[%d]=%d\n",k,k,k);
else
printf("沒有找到任何一個數字滿足a[i]=i\n");
int i,j;
int x;
printf("請輸入你想要查找的數字\n");
scanf("%d",&x);
if(BinarySearch(a,N,x,i,j))
printf("找到了:i和j均在%d\n",j);
else
printf("沒找到:j=%d\ti=%d\n",j,i);
getchar();
getchar();
return 0;
}
E. 我有兩道數據結構的問題 希望能給出詳細解答和做題步驟
數據結構與演算法的地位對於一個程序員來說不言而喻。今天這篇文章不是來勸你們學習數據結構與演算法的,也不是來和你們說數據結構與演算法有多重要。
主要是最近幾天後台有讀者問我是如何學習數據結構與演算法的,有沒有什麼捷徑,是要看視頻還是看書,去哪刷題等.....而且有些還是大三大四的,搞的我都替你們著急、擔心.....
所以我今天就分享下自己平時都是怎麼學習的。
學習演算法的捷徑就是多刷題
說實話,要說捷徑,我覺得就是腳踏實地著多動手去刷題,多刷題。
但是,如果你是小白,也就是說,你連常見的數據結構,如鏈表、樹以及常見的演算法思想,如遞歸、枚舉、動態規劃這些都沒學過,那麼,我不建議你去刷題的。而是先去找本書先去學習這些,然後再去刷題。
也就是說,假如你要去諸如leetcode這些網站刷題,那麼,你要先具備一定的基礎,這些基礎包括:
1、常見數據結構:鏈表、樹(如二叉樹)。
2、常見演算法思想:貪婪法、分治法、窮舉法、動態規劃,回溯法。
以上列出來的算是最基本的吧。就是說你刷題之前,要把這些過一遍再去刷題。如果你連這些最基本的都不知道的話,那麼你再刷題的過程中,會很難受的,思路也會相對比較少。
總之,千萬不要急,先把這些基本的過一遍,力求理解,再去刷題。這些基礎的數據結構與演算法,我是在大一第二學期學的,我沒看視頻,我是通過看書學的,那時候看的書是:
1、演算法分析與分析基礎:這本比較簡單,推薦新手看。
2、數據結構與演算法分析---C語言描述:代碼用C寫的,推薦看。
3、挑戰程序設計競賽(第二版):也是很不錯的一本書,推薦看。