第二章的主要學習內容是線性表;
在這里我想回顧從開學到現在所學的所有知識;
第一章 緒論
1、學習了一些基本的概念和術語
(1)數據、數據元素、數據項和數據對象
1)數據(Data):是客觀事物的符號表示,是所有輸入到計算機中的並被計算機程序處理的符號的總稱;
如數學計算機中用到的整數和實數、多媒體程序處理的圖形、圖像、聲音及動畫等通過特殊編碼定義后的數據;
2)數據元素(Data Element):是數據的基本單位,在計算機中通常作為一個整體進行考慮和處理。數據元素用於完整地描述一個對象。
如一名學生的記錄、圖中的一個格局;
3)數據項(Data Item):是組成數據元素的、有獨立含義的、不可分割的最小單位。
例如,學生基本信息表中的學號、姓名、性別等都是數據項。
4)數據對象(Data Object) :是性質相同的數據元素的集合,是數據的一個子集。
如學生基本信息表也可以是一個數據對象。
2、學習了數據結構相關知識
(1)數據結構(Data Structure):是相互之間存在一種或多種特定關系的數據元素的集合。
通俗講就是:數據結構是帶“結構”的數據元素的集合,“結構”就是指數據元素之間存在的關系。
分類:
數據結構又分為:邏輯結構和存儲結構兩個層次;
1)邏輯結構有兩個要素:一是數據元素,二是關系;
根據元素之間不同的特性,可分為四類
集合結構(離散關系)、線性結構(一對一關系)、樹結構(一對多關系)、圖結構(多對多關系)
形狀大致如下(用電腦畫圖畫的)畫的不太標准;
線性結構:一對一關系;
圖結構或網狀結構:存在多對多關系;

2)存儲結構
存儲結構分為順序存儲結構和鏈式存儲結構;
存儲結構也稱為物理結構
(1)順序存儲結構: 數據元素與數據元素之間的存儲空間連續;
優點:隨機存取(查詢);
缺點:不方便插入、刪除、擴容;
圖如下:
(2)鏈式存儲結構:數據元素與數據元素之間的存儲空間可以不連續;
鏈式存儲結構是順序存取;
優點 :方便插入、刪除、擴容;
缺點:不方便查詢;
🔺每個結點占用兩個連續的存儲單元:一個存放結點的信息,另一個存放后繼結點的首地址;每個結點附加一個“下一個結點地址”,即后繼指針字段,用於存放后繼結點的首地址;
圖如下:
抽象數據類型(Abstract Data Type,ADT) 具體包括三個部分:數據對象、數據對象上關系的集合以及數據對象的基本操作的集合
3、學習了算法和算法分析。
(1)算法(Algorithm):是為了解決某類問題而規定的一個有限長的操作序列;
五個特性: 1)有窮性; 2)確定性; 3)可行性; 4)輸入 ;5)輸出;
(2)評價算法優劣的基本標准
1)正確性 2)可讀性 3)健壯性 4)高效性;
🔺(3)算法的時間復雜度:執行算法所需的計算工作量; T(n)
1)影響算法時間代價的最主要因素是問題規模。
問題規模是求解問題的輸入量的多少,是問題大小的本質表示,一般用整數n表示。
2)一個算法的執行時間大致等於其所有語句執行的時間的總和,而語句的執行時間則為該條語句的重復執行次數和執行一次所需時間的乘積;
3)算法的時間復雜定義
所謂“基本語句” 指的是算法中重復執行次數和算法的執行時間成正比的語句;它對算法運行時間的貢獻最大;
🔺我們用“O”來表示數量級
一般情況下,算法中的基本語句重復執行的次數是問題規模n的某個函數f(n)的增長率相同,算法的時間量度記作 T(n) = O(f(n));
它表示隨問題規模n的增大,算法執行時間的增長率和f(n)的增長率相同,稱作算法的漸進時間復雜度,簡稱時間復雜度(Time Complexity);
例 常量階示例
{ x++;s = 0;}
兩條語句頻度均為1,算法的執行時間是一個與問題規模n無關的常數,所以算法的時間復雜度為T(n) = O(1),稱為常量階;
🔺實際上,如果算法的執行時間不隨問題規模n的增加而增長,算法中的語句頻度就是某個常數,即使這個常數再大,算法的時間復雜度都是O(1)。
例 for( i = 0 ; i < 1000 ;i++) { x++; s = 0 ;} 該算法的時間復雜度仍然為O(1);
例 線性階示例
例 for ( i = 0 ; i < n ; i++) { x++; s = 0;} 循環體內兩條基本語句的頻度均為f(n) = n,所以算法的時間復雜度為T(n) = O(n),稱為線性階;
例 平方階示例
例 x = 0 ; y = 0 ; ...(1)
for (k = 1 ; k <= n ;k++) ...(2)
x++; ...(3)
for(i = 1 ; i <= n;i++) ...(4)
for(j = 1;j <= n;j++) ...(5)
y++; ...(6)
以上程序段中頻度最大的是語句(6),其頻度為f(n) = n² ,所以該算法的時間復雜度為T(n) = O(n²),成為平方階;
🔺多數情況下,當有若干個循環語句時,算法的時間復雜度是由最深層循環內的基本語句的頻度f(n)決定的;
(4)最好、最壞和平均時間復雜度
1)最好時間復雜度:算法在最好情況下的時間復雜度,指的是算法計算量可能達到的最小值;
2)最壞時間復雜度:算法在最壞情況下的時間復雜度,指的是算法計算量可能達到的最大值;
3)平均時間復雜度:算法在所有可能的情況下,按輸入實例以等概率出現時,算法計算量的加權平均值;
我們常常考慮的是最壞的時間復雜度。
假設我們以數組查詢某個數為例;
例:對於數組查詢,(假設數組有n個元素,我們要在數組中找一個數),
最好的情況下是:第一個便是我們要查找的,時間復雜度為O(1);
最壞的情況下是:找到最尾端,第n個值才是我們要查找的,時間復雜度為O(n);也可能找到末端都找不到這個數,也就是數組中沒有這個數;
平均時間復雜度:O((1+n)/2)≈ O(n/2),
(5)算法的空間復雜度
1)類似於算法的時間復雜度,我們采用漸進空間復雜度作為算法所需存儲空間的量度,簡稱為空間復雜度,它也是問題規模n的函數,記作 : S(n) = O(f(n));
2)若算法執行時所需要的輔助空間相對於輸入數據量而言是個常數,則稱這個算法為原地工作,輔助空間為O(1);
下面以數組逆序為例;例 1 例2
1 for( i = 0 ; i < n/2 ;i++) for( i = 0 ; i < n ;i++) 2 { t = a[i]; { b[i] = a[n-i-1]; 3 a[i] = a[n-i-1]; for( i = 0 ; i < n ;i++); 4 a[n-i-1] = t; a[i] = b [i]; 5 } }
例1 僅需要另外借助一個變量t ,與問題規模n大小無關,所以其空間復雜度為O(1);
例2 需要另外借助一個大小為n的輔助數組b,其空間復雜度為O(n);
總結:當追求一個較好的時間復雜度時,可能會導致占用較多的存儲空間,即可能使空間復雜度性能變差,反之亦然;
不過通常情況下,鑒於運算空間較為充足,人們都以算法的時間復雜度作為優劣的衡量指標。
第二章 線性表
1、學習了線性表的定義和特點
線性表可用順序表和鏈表表示;
(1)線性表:由n(n>=0)個數據特性相同的元素構成的有限序列;
稱作順序存儲結構或順序映像; 像這種存儲結構的線性表稱為順序表(Sequential List)
則存儲位置的表示有下列關系 : LOC(a(i+1)) = LOC(a(i) + L;
🔺只要確定了存儲線性表的起始位置,線性表中的任一數據元素都可以隨機存取;所以線性表的順序存儲結構是一種隨機存取的存儲結構。
假設元素的類型為int型
1 #define max 100 2 typedef struct 3 { 4 int *elem; //存儲空間的基地址; 5 int length //當前長度; 6 }sqList; //為這個結構題起了個類型名叫做SqList;
上述代碼也可以寫成
1 #define max 100 2 struct sqList 3 { 4 int *elem; //存儲空間的基地址; 5 int length //當前長度; 6 };
更簡便;
🔺假設以數組為例,這里要特別注意數據過大的情況;
解決方案:1)定義為全局變量; 2)動態申請; int *a = new int[ ]; 注意如果動態申請空間,最后一定要delete,如果不delete,則稱為內存泄漏。
3、學習了順序表中基本操作的實現;
1)初始化;//構造一個空的順序表
1 2 #define max 100 3 4 struct SqList 5 6 { 7 8 int *elem; //存儲空間的基地址; 9 10 int length //當前長度; 11 12 }; 13 SqList L; 14 bool InitList (SqList L) 15 { 16 L.elem = new int [max]; //為順序表分配一個大小為max的數組空間; 17 if(L.elem==NULL) exit(0); //存儲分配失敗退出 18 L.length = 0; //初始化表的長度為0;即為空表; 19 return true; 20 }
動態分配線性表的存取區域可以更有效的利用系統資源,當不需要該線性表的時,可以使用delete操作及時釋放占用的存儲空間。
2)取值 假設取第i個元素;
我們以下標從0 開始,一共n個元素;
1 bool getElem(SqList L, int i ; int tmp) 2 { 3 if( i < 0 || i >= L.length) //判斷 i 的值是否合理 ,如果 i < 0 或者 i >= L.length 則越界了。 4 return false; 5 tmp = L.elem[ i - 1]; //取第 i 個元素,下標時 i - 1; 6 return true; 7 }
3)查找 假設元素為int 類型
1 int chazhao (SqList L, int tmp) 2 { 3 for( i = 0 ; i < L.length ;i++) 4 if(L.elem[i] == tmp) return i + 1; 5 else return -1; 6 }
分析:從第一個元素開始,依次與tmp比較,若找到與tmp 相等的元素L.elem[i],則查找成功,返回該元素的序號 i+1;
若查遍整個順序表都沒有找到,則查找失敗,返回 -1;
🔺算法分析:當在順序表中查找一個元素時,其時間主要耗費在數據的比較上,而比較的次數取決於被查元素在線性表中的位置;
這可以前面那個時間復雜度的分析聯系在一起;
4)插入 假設我們在下標為i的位置插入新的元素tmp;
1 bool LinstInsert (SqList L, int i ,int tmp) 2 { 3 if( i < 0|| i >= L.length) 4 return false; //插入新元素的合理位置是從 i = 0 到 i = L.length - 1 之間,因為我們的下標是從 i= 0 開始; 5 if(L.length == max) 6 return false; //如果順序表的長度已滿,則不能再插入;這個條件要寫在插入之前,因為插入后L.length+1;但是若本身表已滿,我們還插入,就錯誤了; 7 for( int j = L.length - 1 ; j >= i; j--) 8 L.elem [ j + 1 ] = L.elem [ j ]; // 插入位置及之后的元素都要后移; 9 L.elem[i] = tmp; 10 L.length++; //添加一個元素,表長+1; 11 return true; 12 }
🔺算法分析:當順序表中某個位置上插入一個數據元素時,其時間主要耗費在移動元素上;而移動元素的個數取決於插入元素的位置;
與前面的時間復雜度的分析聯系在一起;
5)刪除 在下標為i的位置刪除一個元素
1 bool ListDelete (SqList L,int i) 2 { 3 if( i < 0 || i >= L.length) //刪除元素的合理位置是從 i = 0 到 i = L.length - 1 之間,因為我們的下標是從 i= 0 開始; 4 return false; 5 for( int j = i ; j < L.length ;j++) 6 { 7 L.elem[ j - 1 ] = L.elem [ j ]; 8 } 9 L.length --; 10 return true; 11 }
4、學習了線性表的鏈式表示和實現
1)特點:邏輯上相鄰,物理存儲結構可以不相鄰;
2)除了存儲本身的信息外,還需要存儲一個指示其直接后繼的信息 ,示意圖大致如下:
這兩部分信息組稱數據元素ai的存儲映像,稱為結點(node)。它包括兩個域:
(1)存儲數據元素的域稱為數據域 ; (2)存儲直接后繼存儲位置的域稱為指針域; 指針域中存儲的信息稱作指針或鏈。
n個結點鏈結成一個鏈表,即為線性表;
由於此鏈表的每個結點只包含一個指針域,故又稱為線性鏈表或單鏈表;
單鏈表中的最后一個結點的指針為空(NULL);
(2)單鏈表可由頭指針唯一確定; 由下圖可看出; 下圖是只有頭指針沒有頭結點的示意圖;
單鏈表的存儲結構: 這里假設元素數據類型為 int 類型;
1 typedef struct LNode 2 { 3 int data ; //這個結點的數據; 4 struct LNode *next; //這個結點的指針域 ,用next表示 5 }LNode,*LinkList; //給LNode起了兩個別名,叫做LNode 和LinkList;
實際上我們寫下面那種方式更加簡便 直接寫個結構體,不用起別名;
1 struct LNode 2 { 3 int data; //這個結點的數據; 4 struct LNode *next; //這個結點的指針域 ,用next表示 5 }
一般情況下,為了處理方便,在單鏈表的第一個結點之前附設一個結點,稱之為頭結點 ; 示意圖如下:
鏈表增加頭結點的作用:
1)便於首元結點的處理
增加了頭結點后,首元結點的地址保存在頭結點 (即其”前驅“結點)的指針域中,則對鏈表的第一個數據元素的操作與其他數據元素相同,無需進行特殊處理;
2)便於空表和非空表的統一處理
(1)當鏈表不設頭結點,假設L為單鏈表的頭指針,它應該指向首元結點,則當單鏈表為長度n為0的空表時,L指針為空(判定空表的條件可記為:L == NULL);這里的L是頭指針;
不設頭結點的非空表;
不設頭結點的空表;
(2)增加頭結點后,無論鏈表是否為空,頭指針都是指向頭結點的非空指針。
如下圖 ,非空單鏈表,頭指針指向頭結點。 空表,則頭結點的指針域為空,(判定空表的條件可記為:L->next == NULL);
設頭結點的非空表;
設頭結點的空表;
5、學習了單鏈表基本操作的實現 下面都是以帶頭結點來實現;
(1)初始化
1 struct LNode 2 { 3 int data; //這個結點的數據; 4 struct LNode *next; //這個結點的指針域 ,用next表示 5 } 6 7 LNode *L ; //定義一個類型為LNode 的頭指針L; 8 bool InitList (LNode *L) //傳入頭指針 9 { 10 L = new Node; //為頭指針開辟一個存儲空間; 11 L-> next = NULL; //頭結點的指針域為空,構造一個空的單鏈表 12 return true; 13 }
可看下面的圖方便理解;
2)取值
1 bool GetElem( LNode *L ; int i ; int tmp) // 傳入三個參數,第一個是L頭指針,第二個是取第 i 個元素 , 第三個是傳入tmp ,方便后面將第i個元素賦值給tmp 2 { 3 LNode *p ; //定義一個移動的指針; 4 p = L ->next ; // 初始化時, 將L->next賦值給p,使 p 指向首元結點; 可見下圖; 5 int j = 1; //將 j 初值賦為1 ;方面和 計算與 i 的關系; 6 while( p ! = NULL&& j <i ) //順鏈域向后掃描,直到p為空或者p指向第i個元素; 7 { 8 p = p -> next; //p指向下一個結點; 9 j++; // 計數器 j 相應加1; 10 } 11 if( p == NULL || j > i) //當上面的while循環結束后 p 已為空或者 j 已經大於 i 了 則找不到第 i 個元素; 取值失敗; 12 return false; 13 tmp = p -> data; //找到第 i 個元素, 將其 賦值給tmp ; 14 return true; 15 }
🔺 算法分析:該算法的基本操作是比較 j 和 i 並 后移指針 p 。while 循環體中的語句頻度與位置 i 有關。 若 1 <= i <= n;則頻度為 i - 1,一定能取值成功;
若 i > n ,則頻度為 n ,取值失敗,因此算法的時間復雜度為 O(n);
3)查找 下面代碼返回的是一個指針
1 LNode *LocateElem (LNode *L , int tmp ) //傳入頭指針和要查找的值; 2 { 3 p = L -> next; //初始化,p指向首元結點,與上面取值是類似的; 4 while( p != NULL && p -> data != tmp) //順鏈表向后掃描,直到p為空或者p所指結點的數據域等於tmp; 5 { 6 p = p -> next; //p指向下一個; 7 } 8 return p; //查找成功返回數值為tmp的結點地址p; 查找失敗則返回NULL; 9 }
🔺算法分析:該算法的執行時間與待查找的tmp值由關,其時間復雜度也為O(n)
4)插入
1 bool ListInsert (LNode *L; int i ; int tmp) //傳入頭指針, 傳入i,指在第i個插入,傳入插入的值tmp; 2 { 3 LNode *p ; //定義一個可移動的指針; 4 LNode *s; //定義一個指向tmp的指針; 5 p = L; //將p指向頭指針; 6 int j = 1; //將 j 初值賦值為1 ;起一個計數器的作用; 7 while(p != NULL && j < i-1) 8 { 9 p = p ->next; //p指針順着鏈表不斷后移; 10 j++; // j 的記數相應+1; 11 } 12 if( p == NULL || j > i-1) //如果找到最后找不到,p指針指向空 或者 j 已經大於 i-1 了 ,則插入失敗; 13 return false; 14 //若沒有進入上面那個if 中,則可插入 ,則 15 s = new LNode; //為新元素開辟一個空間; 16 s ->data = tmp; //將tmp 的指賦給該結點的數據域; 17 s -> next = p -> next; //在i - 1 和 i 之間插入, 則將指針指向換一個即可;下面會有圖解釋這里; 18 p -> next = s; 19 return true; //插入成功,返回true; 20 } 21
插入前:
插入后:
🔺算法分析:單鏈表的插入操作雖然不需要像順序表那樣插入后要移動,但是時間復雜度仍為O(n);
因為為了在第 i 個結點之前插入一個新結點 ,必須先找到 i - 1 個結點;
5)刪除
1 bool ListDelete(LNode *L , int i ) //傳入兩個參數,一個是頭指針,一個是在第 i 個位置刪除的 i; 2 { 3 LNode *p ; //定義一個可移動的指針; 4 int j = 1; //將 j 的初值賦值為1 ; 5 LNode * tmp; //定義一個暫時的中間指針; 6 while( p ! = NULL && j < i ) 7 { 8 p = p -> next ; //p 順着鏈表向后移動; 9 j++; //計數器 j 相應加1; 10 } 11 if( p == NULL || j > i) //若p找到最后都指向空了還找不到 i 或者 j > i 了,則刪除失敗; 12 return false; 13 tmp = p ->next; 14 p->next = tmp ->next; 15 16 delete tmp; 17 return true; 18 }
刪除前:
刪除后:
6)創建單鏈表 前插法和后插法;
(1)前插法:
1 void CreatList (LNode *L ,int n) 2 { 3 LNode *p; //先定義一個可移動的指針; 4 L = new LNode; 5 L -> next = NULL; //先建立一個帶頭結點的空鏈表; 6 for(int i = 0 ; i < n ; i++) 7 { 8 p = new LNode; //為新結點開辟一個空間; 9 cin>>p -> data; //輸入新結點的數據域; 10 p ->next = L ->next ; // 把L的下一個賦給 p的下一個; 11 L - >next = p; // 把p賦給 L 的下一個; 12 } 13 }
🔺算法分析:時間復雜度為O(n);
(2)后插法:
1 void CreatList(LNode *L ,int n ); //傳入頭指針 ,和長度; 2 { 3 LNode *tail; //實際上這是個尾指針; 4 LNode *tmp //定義一個暫時中間的指針; 5 L = new LNode; 6 L->next = NULL; 7 tail = L; //初始化,尾指針和頭指針指向相同; 8 for(int i = 0 ; i < n ; i ++) 9 { 10 tmp = new LNode ; //每新插入一個結點,為它開辟一個空間; 11 cin>>tmp->data; //輸入新結點的數據域的數值; 12 tmp ->next = NULL; //尾插,最后tmp的next是為空的; 13 tail ->next = tmp; //tail 指向新的尾結點tmp; 14 } 15 }
🔺算法分析:時間復雜度為O(n);
除此之外還簡單學習了循環鏈表和雙向鏈表,理解較粗淺,就不在這里寫了;
6、作業:pintia上第二章的作業題是建立順序表和鏈表,具體詳細的知識在上面都有;
7、雙向鏈表的建立。這里先不展開,以后有精力再寫有關內容。有同學寫解釋的挺好的,所以這里就先不寫出來了。
8、作業:pintia上的實踐題;
題目描述如下:
7-1 還是求集合交集 (30 分)
給定兩個整數集合(每個集合中沒有重復元素),集合元素個數<=100000,求兩集合交集,並按非降序輸出。
輸入格式:
第一行是n和m,表示兩個集合的元素個數; 接下來是n個數和m個數。
輸出格式:
第一行輸出交集元素個數; 第二行按非降序輸出交集元素,元素之間以空格分隔,最后一個元素后面沒有空格。
輸入樣例:
在這里給出一組輸入。例如:
5 6
8 6 0 3 1
1 8 9 0 4 5
輸出樣例:
在這里給出相應的輸出。例如:
3
0 1 8
解題思路:這里不能暴力的就排序找相同,因為題目給的數據比較大,會超時;
而且如果寫選擇排序或者冒泡排序的話,兩個for循環時間復雜度上到n²去了,也就是10000000000;而題目給的是0.4s 肯定超時;
解法一:
排序的話可以之間用sort 時間復雜度是logn;
然后比較的話,可先將兩個集合先排序再比較,優化了算法;

代碼如下:
1 #include<iostream> 2 #include<algorithm> 3 using namespace std; 4 5 6 7 int a[200000]; 8 int b[200000]; 9 int c[200000]; 10 int count1 = 0; 11 int main() 12 { 13 int n , m ; 14 cin>>n>>m; 15 for(int i = 0 ; i < n ;i++) 16 { 17 cin>>a[i]; //輸入a集合 18 } 19 for(int j = 0 ; j < m; j++) 20 { 21 cin>>b[j]; //輸入b集合 22 } 23 sort(a,a+n); //對於a集合排序 ,sort 頭文件為algorithm 24 sort(b,b+m); //對於b集合排序 25 //對兩個集合排序是為了后面方便比較 26 int i = 0 ; 27 int j = 0; 28 while(i < n && j < m) 29 { 30 if(a[i]<b[j]) 31 { 32 i++; //一對一進行比較,直到找到相同 33 }else 34 if(b[j]<a[i]) 35 { 36 j++; //同理,一對一進行比較,直到找到相同 37 } 38 else 39 if(a[i]==b[j]) 40 { 41 c[count1] = a[i];//如果兩個均相同,則兩個均向前移動,c集合可將a集合對應的數放進去,也可將b集合對應的數放進去 42 count1++; 43 i++; //a集合對應的向前移動 44 j++; //b集合對應的向前移動 45 } 46 } 47 cout<<count1<<endl; 48 for(int i = 0 ; i < count1 ;i++ ) 49 { 50 cout<<c[i]; //輸出c集合 51 if(i!=count1-1) 52 cout<<" "; //注意規范 53 } 54 return 0; 55 }
解法二:在輸入a集合的同時將a中的元素標記,在輸入b集合的同時看是否有與a集合相同的元素,有的話就存到c集合中去,這里不能 用單純的int 數組標記,因為會數字太大,會爆掉;所以可用map來標記;、
1 #include<iostream> 2 #include<algorithm> 3 #include<map> 4 using namespace std; 5 6 map<int,int>vis; //利用map數組;頭文件為map 7 int a[200000]; 8 int b[200000]; 9 int c[200000]; 10 int count1 = 0; 11 int main() 12 { 13 14 int n ,m ; 15 cin>>n>>m; 16 for(int i = 0 ;i < n ; i++) 17 { 18 cin>>a[i]; 19 vis[a[i]] = 1; //標記a集合中的元素; 20 21 } 22 for(int i = 0 ; i < m ;i++) 23 { 24 cin>>b[i]; 25 if(vis[b[i]]==1) //如果b集合中有與a集合相同的元素 ,則存到c集合中; 26 { 27 c[count1] = b[i]; 28 count1++; 29 } 30 } 31 32 33 sort(c,c+count1); //排序; 34 cout<<count1<<endl; 35 for(int i = 0 ; i < count1 ;i++) 36 { 37 cout<<c[i]; 38 if(i!=count1-1) 39 { 40 printf(" "); 41 }else cout<<endl; 42 43 } 44 }
雖然總的這篇回顧寫了10多個小時,但是覺得還是有所收獲!
這段時間的不足之處:對鏈表的掌握程度一般,不是那么熟練;
下一章的學習:在熟練掌握鏈表的同時,將棧與隊列熟練掌握,靈活運用。