資料結構第一章 緒論 基本概念與演算法分析
資料結構
1.資料
- 數值型
- 1 2 3
- 非數值型
- 文字,影象, 圖形
2.資料元素和資料項
-
資料元素
-
組成資料的基本單位就是資料元素
-
例如
學號 姓名 性別 出生日期 政治面貌 0001 張三 男 20020514 團員 0002 李四 男 20030604 團員 0003 王五 男 20040507 團員 每一行就是一個數據元素,或者稱為記錄,節點或頂點
-
-
-
資料項
- 組成資料元素的不可分割的最小單位
- 例如上面的表中每一列就是一個數據項
- 組成資料元素的不可分割的最小單位
-
資料 >資料元素>資料項
- 學生表>個人紀錄>學號,姓名·····
3.資料物件
- 是性質相同的資料元素的集合,是資料的一個子集
- 例如整數的資料物件是集合N={1,2,3··}
4.資料物件與資料元素
- 資料元素 ------ 組成資料的基本單位
- 與資料的關係: 是集合的個體
- 資料物件 ----- 性質相同的資料元素 的集合
- 與資料的關係是:集合的子集
5.資料結構
-
資料結構
-
資料元素不是孤立存在的,他們之間存在著某種關係,資料元素相互之間的關係成為結構
-
是指相互之間存在著一種或者多種特定關係的資料元素的集合
-
是帶有結構的資料元素的集合
-
-
資料結構包括的內容
- 資料元素之間的邏輯關係,稱為邏輯結構
- 資料元素及其關係在計算機記憶體中表示(又稱為映像),稱為資料機構的物理結構或者資料的儲存結構
- 資料的運算和實現,及對資料元素可以施加的操作在相應的儲存結構上的實現
資料結構的兩個層次
- 邏輯結構
- 描述資料元素之間的邏輯關係
- 與資料的儲存無關,獨立於計算機
- 是從具體問題抽象出來的數學模型
- 物理結構(儲存結構)
- 資料元素及其關係在計算機儲存器中的結構(儲存方式)
- 是資料結構在計算機中的表示
- 邏輯機構與儲存結構的關係
- 儲存結構時邏輯結構關係的映像與元素本身的映像
- 邏輯結構是資料結構的抽象,儲存結構是資料結構的實現
- 兩者綜合起來建立了資料元素之間的結構關係
邏輯機構的種類
劃分方法一
- 線性結構
- 有且僅有一個開始和一個終端節點並且所有節點最多隻有一個直接前趨和一個直接後續
- 例如:線性表,棧,佇列,串
- 非線性結構
- 一個節點可能有多個直接前趨,和直接後續
- 例如:樹, 圖
劃分方法二 --- (四類基本邏輯結構)
- 集合機構:資料中的資料元素之間除了同屬與一個集合的關係外,無任何其他關係
- 線性結構:結構中的資料元素之間存在著一對一的線性關係
- 樹形結構:結構中資料元素存在著一對多的層次關係
- 圖狀結構或網狀結構:機構中的資料元素之間存在這多對多的任意關係
- )
儲存結構
1.順序結構
- 用一組連續的儲存單元依次儲存資料元素,資料元素之間的邏輯關係由元素的儲存位置來表示
- C語言中的資料就是這樣儲存的
2.鏈式結構
- 用一組任意的儲存單元儲存資料元素,資料元素之間的邏輯關係使用指標來表示
- c語言中指標來實現鏈式儲存結構
3.索引儲存結構
- 在儲存節點資訊的同時,還建立了附加的索引表,索引表中的每一項稱為一個索引項
- 索引項的一般形式為:(關鍵字,地址)
- 關鍵字是能唯一標識一個節點的那些資料項
- 每一個節點在索引表中都有一個索引項,則該索引表稱為稠密索引。若一組節點在索引表中只對應一個索引項,則該索引表稱為稀疏索引
4.雜湊儲存結構
- 根據節點的關鍵字直接計算出該節點的儲存地址
資料型別和抽象資料型別
- 在使用高階程式語言設計語言編寫程式時,必須對程式中出現的每個變數。常量,或表示式,明確說明他們所屬的資料型別
- 列入在c語言中
- 提供int char float double等基本資料型別
- 陣列,結構體,共用體,列舉等構造資料型別
- 還有指標,空(void)型別
- 使用者也可以使用typedef自己定義資料型別
- 一些最基本資料結構可以用資料型別類實現,如資料,字串等
- 而另一些常用的資料結構,如棧,佇列,樹,圖等。不能直接使用資料型別來表示
- 列入在c語言中
- 高階語言中的資料型別明顯的或隱含的規定,在程式執行期間變數和表達的所有可能的取值範圍,以及在這些資料範圍山所允許進行的操作
- 例如int型
- 範圍就是-32368 --- 32762
- 在這個整數集上可以進行 + - * / % 等操作
- 例如int型
- 資料型別的作用
- 約束變數或常量的取值範圍
- 約束變數或常量的操作
資料型別
- 定義: 資料型別是一組性質相同的值的集合,以及定義與這個值的集合上的一組操作的總稱
- 資料型別 = 值的集合 + 值集合上的一組操作
1.抽象資料型別(Abstract Data Type, ADT)
- 定義: 是指一個數學模型以及定義在此數學模型上的一組操作
- 用使用者定義,從問題抽象出資料模型(邏輯結構)
- 還包括定義在資料模型上的一組抽象運算(相關操作)
- 不考慮計算機內的具體儲存結構與運算的具體實現演算法
2.抽象資料型別的定義形式
抽象資料型別可用(D,S,P)三元組表示 |
---|
其中:D 是資料物件 |
S 是D上的關係集 |
P 是對D的基本操作集 |
-
一個抽象資料型別的定義格式如下
ADT 抽象資料型別名
{
資料物件:<資料物件的定義>
資料關係:<資料關係的定義>
基本操作:<基本操作的定義>
} ADT 抽象資料型別名
-
其中資料物件,資料關係的定義用虛擬碼描述
-
基本操作的定義格式為:
- 基本操作名(引數表)
- 賦值引數, 只為了操作提供輸入值
- 例如 圓的面積: area(a)
- 引用引數 以&打頭,去可提供輸入值外,還將返回操作結果
- 例如 圖形的縮放:picture(&G, n);將縮放後的圖形依舊儲存在G中,類似c語言中以指標為引數傳遞的自定義函式,
- 賦值引數, 只為了操作提供輸入值
- 初始條件(初始條件描述)
- 描述操作執行前資料和引數應滿足的條件,如不滿足,則操作失敗,並且返回對應出錯的資訊,如初始條件為空,則省略之
- 操作結果(操作結果描述)
- 說明操作正常完成後,資料結構的變化狀況和應返回的結果
- 基本操作名(引數表)
-
例
ADT Circle
{
資料物件: D = {r , ,x , y | r, x, y 均為實數}
資料關係: R = {<r, x,y>|r是半經,<x,y>是圓心座標}
基本操作:
Circle(&C,r,x,y)
操作結果:構造一個圓。
double Area(C)
初始條件:圓已存在。
操作結果:計算面積。
double Circumference(C)
初始條件:圓已存在
操作結果:計算周長
·······
}ADT Circle
複數定義
ADT Complex
{
D = {r1, r2| r1,r2都是實數}
S = {<r1, r2>| r1是實部, r2是虛部}
assign(&C, v1, v2)
初始條件:空的複數C已存在
操作結果:構造複數C, r1,r2分別被賦予引數v1,v2的值
destory(&C)
初始條件:複數C已存在
操作結果: 複數C被銷燬
GetReal(&C, &realPart)
初始條件:複數已存在。
操作結果:用realPart返回複數C的實部值
GetImag(C,&ImagPart)
初始條件:複數已存在
操作結果:用ImagPart返回複數C的虛部值
}ADT Complex
3.抽象資料的具體化
-
c語言實現抽象資料型別
- 使用已有的資料型別定義描述它的儲存結構
- 用函式定義描述它的操作
-
例:使用c語言來對抽象資料型別“複數”的實現
/* 該程式是用來實現複數的運算 本函式用來求複數 ((8+6i)*(4+3i)) / ((8+6i) + (4+3i)) */ #include <stdio.h> typedef struct{ float realpart; float imagpart; }Complex; void creat(Complex *A, float realpart, float imagpart); void add(Complex A, Complex B, Complex *C); void subtract(Complex A, Complex B, Complex *C); void multiply(Complex A, Complex B, Complex *C); int except(Complex A, Complex B, Complex *C); int main() { int a; Complex z1, z2, z3, z4,z; creat(&z1, 8.0, 6.0); creat(&z2, 4.0, 3.0); add(z1, z2, &z3); multiply(z1, z2, &z4); a = except(z4, z3, &z1); if( a == 0) { printf("分母為零運算結束"); } else { printf("%.2f+%.2fi", z1.realpart, z1.imagpart); } } void creat(Complex *A, float realpart, float imagpart) { A->realpart = realpart; A->imagpart = imagpart; } void add(Complex A, Complex B, Complex *C) { C->realpart = A.realpart + B.realpart; C->imagpart = A.imagpart + B.imagpart; } void subtract(Complex A, Complex B, Complex *C) { C->realpart = A.realpart - B.realpart; C->imagpart = A.imagpart - B.imagpart; } void multiply(Complex A, Complex B, Complex *C) { C->realpart = (A.realpart * B.realpart)-(A.imagpart * B.imagpart); C->imagpart = (A.realpart * B.imagpart)+(A.imagpart * B.realpart); } int except(Complex A, Complex B, Complex *C) { if(B.imagpart != 0 || B.realpart!= 0) { C->realpart = (A.realpart * B.realpart+A.imagpart * B.imagpart)/(B.realpart*B.realpart+B.imagpart*B.imagpart); C->imagpart = (A.imagpart * B.realpart-A.realpart * B.imagpart)/(B.realpart*B.realpart+B.imagpart*B.imagpart); return 1; } return 0; }
演算法和演算法分析
演算法的定義
- 對特定問題求解方法和步驟的一種描述, 它是指令的有限**序列每一個指令表示一個或者多個操作
- 簡而言之,演算法就是解決問題的方法和步驟
- steps1·······
- steps2·······
演算法的描述
- 自然語言:英文, 中文
- 流程圖:傳統流程圖,NS流程圖
- 虛擬碼:類語言:類c語言
- 程式程式碼:c語言程式,JAVA語言程式
演算法與程式
-
演算法是解決問題的一種方法或者一個過程,考慮如何將輸入轉換成輸出,一個問題可以有多種演算法
-
程式使用魔種程式設計語言對演算法的具體實現。
程式 = 資料結構 + 演算法
資料結構通過演算法實現操作
演算法根據資料結構設計程式
演算法特性(有五種)
- 有窮性:一個演算法必須總是在執行又窮步後結束,且,每一步都在有窮區間內完成
- 確定性:演算法中的每一條指令必須有確切的含義,沒有二義性,在任何條件下,只有唯一的一條執行路徑,即對於相同的輸入只能得到相同的輸出。
- 可行性:演算法是可執行的,演算法描述的操作可以通過已經實現的基本操作執行有限次來完成
- 輸入:一個演算法有零個或多個輸入
- 輸出:一個演算法有一個或多個輸出
演算法設計的要求
- 正確性(Correctness)
- 正確性:演算法滿足問題要求。能正確解決問題,演算法轉化為程式後需要注意
- 程式中不含有語法錯誤
- 程式對於幾組輸入資料能夠得出滿足要求的結果
- 程式對於精心選擇的,典型的,苛刻且帶有刁難性的幾組輸入資料能夠得出滿足要求的結果
- 程式對於一切合法的輸入資料都能滿足要求的結果
- 通常以第三層意義上的正確性作為衡量一個演算法是否合格的標準
- 正確性:演算法滿足問題要求。能正確解決問題,演算法轉化為程式後需要注意
- 可取性(Readability)
- 演算法主要是為了人的閱讀和交流,其次才是為了計算機執行,因此演算法應該易與人的理解
- 另一方面,晦澀難懂的演算法易於隱藏較多的錯誤而難以除錯
- 健壯性(Robustness)
- 指當輸入非法資料時演算法恰當的做出反應或進行相應的處理,而不是產生莫名其妙的輸出結果
- 處理錯誤的方法,不應是中斷程式,而是返回一個表示錯誤或錯誤性質的值,以便在更高抽象層次上進行處理
- 高效性(Efficiency)
- 要求花費盡量少的時間,和儘量低的儲存需求
評價演算法
- 一個好的演算法首先要具備正確性,然後是健壯性,可讀性,在這幾個方面都滿足的情況下,組要考慮演算法的效率,通過演算法的效率高低來評判不同演算法的優劣程度
- 演算法效率以下兩個方面來考慮:
- 時間效率:指的是演算法所耗費的時間
- 空間效率: 指的是演算法執行過程中所耗費的儲存空間
- 時間效率和空間效率有時候是矛盾的
- 演算法時間效率的度量
- 演算法時間效率可以用依據該演算法在計算機上執行所耗的時間來度量
- 兩種度量方法
- 事後統計
- 將演算法實現,測算其時間和空間開銷
- 缺點:編寫程式實現演算法將花費較多的時間和精力;所得實驗結果依賴於計算機的軟硬體等環境因素,掩蓋演算法本身的優劣
- 事前分析
- 對演算法所耗資源的一種估算方法
- 事後統計
事前分析法(時間效率)
-
一個演算法的執行時間是指一個演算法在計算機上執行所耗費的時間的大致可以等於計算機執行一中簡單的操作(如賦值,比較,移動等)所需的時間與演算法中進行的簡單操作次數乘積
演算法執行時間 = 一個簡單操作所需的時間 * 簡單操作的次數
每條語句執行的次數又稱為語句頻度
一個簡單操作所需要的時間有機器而異,所以可以假設執行每條語句所需要的時間均為單位時間,所以演算法執行的時間就可以轉換為演算法執行次數的比較
for( i = 0; i < n; i++) // 執行n+1次 最後一次還要判斷是否退出 { for( j = 0; j < n; j++) // 執行n*(n+1)次 { printf("我愛資料結構~~~"); // 執行n*n次 for(k = 0; k < n; k++) // 執行n*n*(n+1)次 { printf("有點膩害"); // 執行n*n*n次 } } }
上述程式碼一共執行了2n^3 + 3n^2 + 2*n + 1次
for( i = 0; i < n; i++) { for( j = i; j < n; j++) { printf("我愛資料結構~~~"); } }
上述程式碼一共執行了n+(n-1)+(n-2)+(n-3) ··· + 2 + 1 = (1+n)*n/2 = n/2 + n^2/2 次
我們把演算法所耗費的時間定義為該演算法中每條語句的頻度之和,則上述程式碼時間消耗為T(n^2)
但是以上方法太過於繁瑣, 所以我們只要比較他們的數量級即可
-
若有某個輔助函式fn(n),使得當n趨近於無窮大的時候,T(n)/f(n)的極限值為不等於零的常數,同階無窮小,記作T(n) = O(f(n)) ,稱O(f(n))為演算法的漸進時間複雜度(O是數量級的符號),簡稱時間複雜度
例如上面第一個例子
T(n) = 2n^3 + 3n^2 + 2*n + 1; 當n->∞的時候 T(n)/n^3 -> 2 這表示n充分大時,T(n)與n^3是同階或者同數量級,引入大“O”記號,則T(n)可記作
T(n) = O(n^3)
-
分析演算法時間複雜度的基本方法
- 找出語句頻度最大的那一條語句作為基本語句
- 計算基本語句的頻度得到問題規模n的某個函式f(n) 然後去掉低次項, 和高次項的係數
- 取其數量級用符號“O”表示、
i = 1; while( i < n) { i = i*2; }
執行次數分析 :
- 若迴圈一次 i = 2;
- 若迴圈兩次 i = 2*2;
- 若迴圈三次 i = 2^3;
- 若迴圈x次 i = 2^x;
- 因為 i < n; 所以 2^x <= n ----> x <= log(2)n;
-
注意 有的情況下,演算法中基本操作重複執行的次數隨問題的輸入資料集不同而不同
-
例 順序查詢,在陣列a[i]中查詢值等於e的元素,返回其所在的位置
for(i = 0; i < n; i++) { if( a[i] == e) { return i+1; //找到,則返回是第幾個元素 } } return 0;
最好的情況: 1次
- 指在最好的情況下,演算法的時間複雜度
最壞的情況:n
- 指在最壞的情況下,演算法的時間複雜度
平均時間複雜度為:O(n)
- 指在所有可能輸入例項在等概率出現的情況下,演算法的期望執行時間
-
一般總是考慮在最壞情況的時間複雜度,以保證演算法執行時間不會比它更長
-
-
對於複雜的演算法,可以將它分成幾個容易估算的部分,然後利用大O加法和乘法法則,計算演算法的時間複雜度
-
加法規則
T(n) = T1(n) + T2(n) = O(f(n)) + O(g(n)) = O(max(f(n), g(n)))
-
乘法法則
T(n) = T1(n)T2(n) = O(f(n)) * O(g(n)) = O(g(n)f(n))
-
空間效率
- 空間複雜度:演算法所需儲存空間的度量
- 記作 S(n) = O(f(n))
- 其中n為問題的規模(或大小)
- 演算法要佔據的空間
- 演算法本身要佔據的空間,輸入/輸出,指令,常數,變數
- 演算法要使用的輔助空間
- 例題
// 將一維陣列a中的n個逆序存放到原陣列中
// 演算法一
for(i = 0;i < n/2; i++)
{
t = a[i];
a[i] = a[n-i-1];
a[n-i-1] = t; //其中t為輔助空間
}
//演算法二
for(i = 0;i < n; i++)
{
b[i] = a[i]; //其中b[i]為輔助空間
}
for( i = 0; i < n; i++)
{
a[i] = b[n-1-i];
}
第一個的空間複雜度為S(n) = O(1);因為輔助空間不隨著n的增長而增長
第二個空間複雜度為S(n) = O(n); n越大b[n]所佔的空間越大
所以第一個演算法比較好