早期人們都把計算機理解爲數值計算工具,感受計算機就是爲了解決複雜計算問題.因此計算機解決問題,應該是先從具體問題中抽象出一個適當的數據模型,設計出一個解決此數據模型的算法,而後纔開始編寫程序,從而實現一個解決問題的軟件.算法
可是,現實開發中,咱們不單純的只是解決數值計算問題,而是須要更加一些高效的手段來解決問題.例如須要更合適的數據結構,樹,表,圖等數據結構來更好的處理問題. 數據結構是一門研究非數值計算的程序設計問題中操做對象.後端
數據結構是一門獨立的學科, 也能夠看出它在計算機專業的地位. 它不是算法的附屬品. 好的程序設計 = 數據結構 + 算法.數組
數據結構和算法是不可分割的. 在你前往高級開發者的路上,數據結構和算法必定是必須啃下來的硬骨頭.bash
數據結構和算法不只是計算機學生的必修課,更是開發者們職業生涯必修課. 你須要不斷的訓練與學習來保持對算法的敏感度.網絡
###1.2 基本概念與術語數據結構
在談數據結構以前,咱們先來談談數據. 正所謂"巧婦難爲無米之炊",再強大的計算機,若是沒有數據. 也是毫無做用.數據結構和算法
數據結構中最基本的5個概念: 數據,數據元素,數據項,數據對象,數據結構;函數
數據: 「是描述客觀事物的符號,是計算機中能夠操做的對象,是能被計算機識別,並輸入給計算機處理的符號集合。數據不只僅包括整型、實型等數值類型,還包括字符及聲音、圖像、視頻等非數值類型。」工具
例如,Mp3就是聲音數據,圖片就是圖像數據. 而這些數據,其實只不過是"符號". 這些符號有2個前提:性能
對於從後端返回的整型,浮點型數據,能夠直接進行數值計算. 對於字符串類型的數據,咱們就須要進行非數值的處理. 而對於聲音,圖像,視頻這樣的數據,咱們須要經過編碼的手段將其變成二進制數據進行處理.
數據元素: 是組成數據的,且有必定意義的基本單位,在計算機中一般做爲總體處理. 也被稱做"記錄"
例如,咱們生活的圈子裏.什麼叫數據元素了? 人Person ,汽車Car 等.
數據項: 一個數據元素能夠由若干數據項組成.
好比,Person 數據元素,能夠爲分解爲眼睛,耳朵,鼻子,嘴巴,手臂這些基本的數據項,也能夠從另外的角度拆解成姓名,年齡,性別,出生地址,出生日期,聯繫電話等數據項. 那麼你如何拆解數據項, 要看你的項目來定.
數據項是數據不可分割的最小單位. 在咱們的課程中,咱們把數據定位爲最小單位.這樣有助於咱們更好理解以及解決問題.
數據對象: 是性質相同的數據元素的集合,是數據的子集.
那麼什麼叫性質相同? 是指數據元素具備相同數量和類型的數項. 相似數組中的元素保持性質一致.
結構,簡單理解就是關係. 好比分子結構,就是說組成分子原子的排列方式. 不一樣數據元素之間不是獨立的,而是存在特定的關係.咱們將這些關係成爲結構. 那麼數據結構是什麼? 數據結構是相互之間存在一種或多種特定關係的數據元素的集合.
代碼片斷(1)
//
// main.c
// 001--數據結構基本術語
//
// Created by CC老師 on 2019/8/12.
// Copyright © 2019年 CC老師. All rights reserved.
//
/*
數據: 程序的操做對象,用於描述客觀事物.
數據的特色: 1️⃣ 能夠輸入到計算機 2️⃣ 能夠被計算機處理
數據項: 一個數據元素由若干數據項組成
數據元素: 組成數據的對象的基本單位
數據對象: 性質相同的數據元素的集合(相似於數組)
結構: 數據元素之間不是獨立的,存在特定的關係.這些關係便是結構;
數據結構:指的數據對象中的數據元素之間的關係
*/
#include <stdio.h>
//聲明一個結構體類型
struct Teacher{ //一種數據結構
char *name; //數據項--名字
char *title; //數據項--職稱
int age; //數據項--年齡
};
int main(int argc, const char * argv[]) {
struct Teacher t1; //數據元素;
struct Teacher tArray; //數據對象;
t1.age = 18; //數據項
t1.name = "CC"; //數據項
t1.title = "講師"; //數據項
printf("老師姓名:%s\n",t1.name);
printf("老師年齡:%d\n",t1.age);
printf("老師職稱:%s\n",t1.title);
return 0;
}
複製代碼
根據視角不一樣,咱們將數據結構分爲2種: 邏輯結構與物理結構;
邏輯結構: 指的是數據對象中的數據元素之間的相互關係. 邏輯結構分爲四種: 集合結構,線性結構,樹形結構,圖形結構. 具體採用什麼的數據結構,是須要根據實際業務需求來進行合理的設計.
集合結構: 集合結構中的數據元素除了同屬於一個集合外,它們之間沒有其餘關係. 各個數據元素是"平等"的. 它們的共同屬性是:"同屬於一個集合".
例如: 動物園,就是一個集合; 河馬,熊貓,獅子,老虎,長頸鹿他們就是數據元素. 它們共同的屬性就是:"同屬於動物這個集合";
線性結構: 線性結構中的數據元素之間是一對一的關係.經常使用的線性結構有:線性表,棧,隊列,雙隊列,數組,串。
樹型結構: 重要的非線性數據結構。樹形數據結構能夠表示數據表素之間一對多的關係.樹型結構中的數據元素之間存在一種一對多的層次關係. 常見的樹形結構: 二叉樹,B樹,哈夫曼樹,紅黑樹等.
圖形結構: 圖形結構的數據元素是多對多的關係. 常見的圖形結構: 鄰近矩陣,鄰接表.
物理結構,別稱"存儲結構". 顧名思義,指的是數據的邏輯結構在計算機的存儲形式.
設計好邏輯數據結構以後,數據的存儲也是很是重要的. 數據存儲結構應該正確反映數據元素之間的邏輯關係.這纔是關鍵! 如何存儲數據元素之間的邏輯關係,是實現物理結構的重點和難點.
數據元素的存儲結構形式有2種: 順序存儲和鏈式存儲;
順序存儲結構: 是指把數據元素存放在地址連續的存儲單元裏,其數據間的邏輯關係和物理關係是一致的.
解讀:
這樣的存儲方式,實際很是簡單. 能夠理解爲排隊佔位. 按照順序,每一個一段空間. 咱們所學習且使用的數組就是典型順序存儲結構. 當你在計算機創建一個有6個整型數據的數組時,計算機便在內存中找到一片空地.按照一個整型所佔空間大小乘以6.開闢一段連續的內存空間. 而後將數據依順序放在位置上,依次排放.
瞭解到順序存儲結構,若是一切數據都有規律且簡單使用順序存儲結構固然是最合適不過的. 可是實際上,老是有複雜的狀況發生. 對於常常且時差發生變化的數據結構,使用順序存儲就會很是不科學. 那該如何解決了?
鏈式存儲結構: 是把數據元素放在任意的存儲單元裏,這組存儲單元能夠是連續的,也能夠是不連續的. 數據元素的存儲關係並不能反映邏輯關係,所以須要用一個指針存放數據元素的地址,這樣經過地址就能夠找到相關關聯數據元素的位置.
顯然,鏈式存儲就靈活多了. 數據存儲在哪裏不重要的,只要有一個指針存放了相應的地址就能找到它了.
總結 邏輯結構是面向問題的,而物理結構就是面向計算機的. 其基本的目標就是將數據以及邏輯關係存儲到計算機的內存中.
數據類型: 是指一組性質相同值的集合以及定義在此集合的一些操做的總稱.
在C語言中,按照取值不一樣,數據類型能夠分爲2類:
抽象,是抽取出事物具備的廣泛性的本質. 它是抽出問題的特徵而忽略非本質的細節,是對具體事物的一個歸納. 抽象是一種思考問題的方式,它隱藏繁雜的細節,只保留實現目標所必需的信息.
抽象數據類型: 是指一個數學模型以及定義在該模型上的一組操做; 例如,咱們在編寫計算機繪圖軟件系統時,常常會使用到座標. 也就是說,會常用x,y來描述橫縱座標. 而在3D系統中,Z深度就會出現. 既然這3個整型數字是始終出如今一塊兒. 那就能夠定義成一個Point的抽象數據類型. 它有x,y,z三個整型變量. 這樣開發者就很是方便操做Point 數據變量.
抽象數據類型能夠理解成實際開發裏常用的結構體和類; 根據業務需求定義合適的數據類型以及動做.
算法: 是解決特定問題求解步驟的描述,在計算機中表現爲指令的有限序列,而且每條指令表示一個或多個操做.
什麼是算法: 解決特定問題的求解步驟的描述; 而數據結構若是脫離算法,或者算法脫離數據結構都是沒法進行的. 因此在大學課程裏,即使單獨算法課程裏也涵蓋了數據結構的內容.
程序設計 = 數據結構 + 算法
你們在大學期間,學習C語言; 大多數遇到的第一個算法問題即是: 求解從1-N相加的結果; 而最簡單的辦法則是使用C語言模擬從1累積到N這個過程,從而獲得結果;
這樣的解決方案,是算法嗎? 我能夠確定告訴你們是的. 這就是算法; 但此時同窗們,也許會想到用數學的方式解決問題. 好比,使用等差數列解決.
故事: 使用這樣的方式解決數學問題,是在18世紀德國的數學家高斯. 在小學時,被留校. 老師要求孩子們從1累積到100. 當時想出來的辦法.
對比以上2種方式,若是不只僅是累積到100,而是一千. 第一種方式,顯然須要計算機循環1千次來模擬數學計算,而第二種方式確定要比第一種來的快.
到底如何評價一個算法優劣了? 接下來咱們花時間來探討.
什麼是算法? 算法就是描述解決問題的方法; 經過剛剛的例子,咱們能夠知道對於一個給定的問題,是能夠有多種算法來解決;
那我想問問,有沒有通用算法? 換過頭來,有沒有包治百病的藥?
現實開發中會有成千上萬的問題,算法也是變幻無窮,沒有通用的算法能夠解決全部問題. 甚至解決一個小問題,很優秀的算法卻不必定很是適合它;
每一個人對問題的解決方案是不同的,可是咱們能夠根據一些參考來評估算法的優劣;
算法必須具有幾個基本特性: 輸入,輸出,有窮性,肯定性和可行性;
輸入輸出,很好理解. 在解決問題時必須有已知條件,固然有些算法可能沒有輸入. 可是算法至少有一個或多個輸出.不然沒有輸出,沒有結果.你用這個算法幹嘛?
有窮性: 指的是算法在執行有限的步驟以後,自動結束而不會出現無限循環,且每個步驟在可接受的時間內完成.
肯定性: 算法的每個步驟都具備肯定的含義,不能出現二義性; 算法在必定條件下,只有一條執行路徑,相同的輸入只能有惟一的輸出結果.
可行性: 算法的每一步都必須是可行的,換句話說,每一步都能經過執行有限次數完成.
算法的正確性是指算法至少應該具備輸入,輸出和加工處理無歧義性,能正確反映問題的需求,可以獲得問題的正確答案;
正確分爲4個層次: (1).算法程序沒有語法錯誤; (2).算法程序對於合法的輸入數據可以產生知足要求的輸出結果; (3).算法程序對於非法的輸入數據可以得出知足規格說明的結果;(4).算法程序對於精心選擇的,甚至刁鑽的測試數據都有知足要求的輸出結果;
可讀性: 算法設計的另外一個目的是爲了便於閱讀,理解和交流;
可讀性高有助於人們理解算法,晦澀難懂的算法每每隱含錯誤,且不容易發現而且難於調試和修改;
注意, 不要犯初學者的錯誤; 認爲代碼越少,就越牛逼! 若是有這樣的想法, 在團隊協做的今天.再也不是我的英雄主義的時代!
可讀性是算法好壞的很重要的標誌!
一個好的算法還應該能對輸入數據的不合法的狀況作出合適的處理.考慮邊界性,也是在寫代碼常常要作的一個處理; 好比,輸入時間或者距離不該該是負數;
健壯性: 當輸入數據不合法時,算法也能作出相關處理,而不是產生異常和莫名其妙的結果;
生活中,人們但願花最少的錢,用最短的時間,辦最大的事. 算法也是同樣的思想. 用最少的存儲空間,花最少的時間,辦成一樣的事.就是好算法!
使用高級程序語言編寫的程序在計算機上運行時所消耗的時間取決於下列的因素:
第一條,確定是算法好壞的根本; 可是第2條以及第4條.都是要看編譯器的支持以及硬件性能. 也就是說,拋開這些與計算機硬件,軟件有關的因素,一個程序運行時間,依賴於算法的好壞和問題的輸入規模. 所謂問題輸入規模是指輸入量的多少.
一個算法在執行過程當中所消耗的時間取決於如下的因素:
1)依賴於輸入設備的性能,如果脫機輸入,則輸入數據的時間能夠忽略不計。(2)(3)取決於計算機自己執行的速度和編譯程序的性能
習慣上將算法語句重複執行的次數做爲算法的時間量度
//x=x+1; 執行1次
void add(int x){
x = x+1;
}
//x=x+1; 執行n次
void add2(int x,int n){
for (int i = 0; i < n; i++) {
x = x+1;
}
}
//x=x+1; 執行n*n次
void add3(int x,int n){
for (int i = 0; i< n; i++) {
for (int j = 0; j < n ; j++) {
x=x+1;
}
}
}
複製代碼
咱們在分析一個算法的運行時,重要的是把基本操做的數量與輸入規模關聯起來,便是基本操做的數量必須表示成輸入規模的函數;
隨着n值的愈來愈大,算法在時間效率上的差別會愈來愈大;
在進行算法分析時,語句的總執行次數T(n)是關於問題規模n的函數. 進而分析T(n)隨着n變化狀況並肯定T(n)的數量級. 算法的時間複雜度,也就是算法的時間量度. T(n) = O(f(n)).
「它表示隨問題規模n的增大,算法執行時間的增加率和f(n)的增加率相同,稱做算法的漸近時間複雜度,簡稱爲時間複雜度。其中f(n)是問題規模n的某個函數。」
大寫O( )來體現算法時間複雜度的記法,咱們稱之爲大O記法。
剛剛的3個求和算法時間複雜度分別爲O(1),O(n),O(n^2);
//1+1+1 = 3
void testSum1(int n){
int sum = 0; //執行1次
sum = (1+n)*n/2; //執行1次
printf("testSum2:%d\n",sum);//執行1次
}
複製代碼
這個算法的運行次數函數是f(n) = 3;根據咱們大O時間複雜度表示,第一步先把常數項改爲1. 在保留最高階時發現,它根本沒有最高階. 因此這個算法的視覺複雜度爲O(1);
//1+1+1+1+1+1+1 = 7
void testSum2(int n){
int sum = 0; //執行1次
sum = (1+n)*n/2; //執行1次
sum = (1+n)*n/2; //執行1次
sum = (1+n)*n/2; //執行1次
sum = (1+n)*n/2; //執行1次
sum = (1+n)*n/2; //執行1次
printf("testSum2:%d\n",sum);//執行1次
}
複製代碼
事實上,不管常數n是多少.以上的代碼執行3次仍是7次的差別,執行時間恆定.咱們的都稱之爲具備O(1)的時間複雜度.又稱爲"常數階";
線性階的循環結構會複雜不少. 要肯定某個算法的階次,咱們經常須要先肯定某個特定語句或某個語句集的運行次數. 所以,咱們要分析算法的複雜度,關鍵就是要分析循環結構的運行狀況.
void add2(int x,int n){
for (int i = 0; i < n; i++) {
x = x+1;
}
}
複製代碼
這段代碼的循環的視覺複雜度爲O(n).
int count = 1;
while(count < n){
count = count * 2;
}
複製代碼
count = count * 2 ; 每次執行這句代碼,就會距離n更近一步; 也就是說, 有多少個2相乘後大於n,則會退出循環.
因此,這個循環時間複雜度爲: O(logn).
/x=x+1; 執行n*n次
void add3(int x,int n){
for (int i = 0; i< n; i++) {
for (int j = 0; j < n ; j++) {
x=x+1;
}
}
}
複製代碼
以上代碼的循環次數爲O(n^2);
//n+(n-1)+(n-2)+...+1 = n(n-1)/2 = n^2/2 + n/2
void testSum4(int n){
int sum = 0;
for(int i = 0; i < n;i++) //執行n次
for (int j = i; j < n; j++) { //執行n-i次
sum += j;
}
printf("textSum4:%d",sum);
}
複製代碼
因爲當i = 0,內循環執行了n次. 當i=1時,執行n-1次,......當i=n-1次,就執行1次;因此總執行次數爲:n+(n-1)+(n-2)+...+1 = n(n-1)/2 = (n^2) /2 + n/2
i = 0,循環執行次數是 n 次。
i = 1,循環執行次數是 n-1 次。
i = 2,循環執行次數是 n-2 次。
...
i = n-1,循環執行的次數是 1 次。
換算成:
result = n + (n - 1) + (n - 2) … + 1
被加數遞減,抽象爲一個等差數列求n項和的問題,公差爲1,帶入公式,Sn = n(a1 + an ) ÷2
result = (n(n+1))/2 = (n^2+n)/2 = (n^2)/2 + n/2
複製代碼
那咱們採用大O階方法,第一條,沒有加法常數不予考慮; 第二條,只保留最高階項,所以保留 (n^2) /2; 第三條,去除這個相乘的常數,也就是去除1/2. 最終這段代碼的時間複雜度爲O(n^2);
思考: 求得如下函數的時間複雜度
指數階O( 2^n ) 和 階乘階 O(n!) 等除非是很是小的n值,不然哪怕n只有100,都會形成噩夢般的運行時間. 因此這種不切實際的算法時間複雜度,通常都不會考慮且討論.
例如,你們在查找一個n個隨機數字數組中的某個數字,最好的狀況是第一個數字就是, 那麼算法的時間的複雜度爲O(1). 但也有可能這個數字就在最後一個位置上,也就是算法時間複雜度爲O(n). 這是最壞的狀況了.
最壞的狀況運行時間是一種保證, 那就是運行時間將不會比這更壞了. 在應用中,這是一種最重要的需求,一般除非特別指定,咱們提到的運行時間都是最壞狀況下的運行時間.
而,從平均運行時也就是從機率的角度來看,這個數字在每一個位置的可能性都是相同的. 因此平均的查找時間爲n/2次後會發現這個目標元素.
平均運行時間是全部狀況中最有意義的,由於它是指望的運行時間. 現實中,平均運行時間都是經過必定數量的分析估算出來.
對於算法的分析,一種方法就是計算全部狀況的平均值,這種時間複雜度的計算的方法稱爲平均時間複雜度. 另外一種方法是計算最壞的狀況下時間複雜度. 這種方法稱爲最壞時間複雜度. 通常沒有特殊狀況下,都是指最壞時間複雜度.
算法設計有一個重要原則,即空間/時間權衡原則(space/time tradeoff)。
算法的空間複雜度經過計算算法所需的存儲空間實現,算法空間複雜度的計算公式記作: S(n) = n(f(n)),其中,n爲問題的規模,f(n)爲語句關於n所佔存儲空間的函數.
通常狀況下, 一個程序在機器上執行時,除了須要寄存自己所用的指令,常數,變量和輸入數據外,還須要一些對數據進行操做的輔助存儲空間. 其中,對於輸入數據所佔的具體存儲量取決於問題自己,與算法無關. 這樣**==只須要分析該算法在實現時所須要的輔助空間就能夠了==**.
若是算法執行時所須要的輔助空間相對於輸入數據量是一個常數,則成這個算法原地工做,輔助空間爲O(1).
程序空間計算因素:
1. 寄存自己的指令
2. 常數
3. 變量
4. 輸入
5. 對數據進行操做的輔助空間
在考量算法的空間複雜度,主要考慮算法執行時所須要的輔助空間.
空間複雜度計算:
問題: 數組逆序,將一維數組a中的n個數逆序存放在原數組中.
複製代碼
簡單理解一下下面的程序段算法空間的複雜度;
int n = 5;
int a[10] = {1,2,3,4,5,6,7,8,9,10};
//算法實現(1)
/*
算法(1),僅僅經過藉助一個臨時變量temp,與問題規模n大小無關,因此其空間複雜度爲O(1);
*/
int temp;
for(int i = 0; i < n/2 ; i++){
temp = a[i];
a[i] = a[n-i-1];
a[n-i-1] = temp;
}
for(int i = 0;i < 10;i++)
{
printf("%d\n",a[i]);
}
//算法實現(2)
/*
算法(2),藉助一個大小爲n的輔助數組b,因此其空間複雜度爲O(n).
*/
int b[10] = {0};
for(int i = 0; i < n;i++){
b[i] = a[n-i-1];
}
for(int i = 0; i < n; i++){
a[i] = b[i];
}
for(int i = 0;i < 10;i++)
{
printf("%d\n",a[i]);
}
複製代碼
算法(1),僅僅經過藉助一個臨時變量temp,與問題規模n大小無關,因此其空間複雜度爲O(1);
算法(2),藉助一個大小爲n的輔助數組b,因此其空間複雜度爲O(n).
注意,算法的空間複雜度指的並非整個算法在內存佔用空間,而是指的是該算法在實現時所須要的輔助空間就能夠
對一個算法,其時間複雜度和空間複雜度每每會互相影響. 當追求一個較好的時間空間複雜度時,可能會致使佔用較多的存儲空間. 便可能會使用空間複雜度的性能變差.反之亦然. 不過,一般狀況下,鑑於運算空間較爲充足,人們都以算法時間空間複雜度做爲算法優先的衡量指標.