數據結構
代碼所在倉庫
數據結構主要研究非數值計算程序問題中的操作對象以及它們之間的關係 不是研究複雜的算法
數據結構指數據對象中數據元素之間的關係
什麼是數據結構的物理結構
物理結構也就是存儲結構,是數據存儲在計算機存儲內的表示(或影像)
存儲結構可以分爲四大類:順序,鏈式,索引,散列
最經常使用的存儲結構:
順序存儲結構->藉助元素在存儲器中的相對位置來表示元素間的邏輯關係
鏈式存儲結構->藉助元素存儲地址的指針表示數據元素間的邏輯關係。
索引->1**/2**/3**類似於字典
散列->哈希,百度雲上傳資源,雲資源庫一旦有直接調用。
數據結構的運算
增刪改查 排序
算法
算法是特定問題求解步驟的描述
算法特性
- 輸入
算法具有 0 個或多個輸入 - 輸出
算法至少有 1 個或多個輸出 - 有窮性
算法在有限的步驟之後會自動結束而不會無限循環 - 確定性
算法中的每一步都有確定的含義,不會出現二義性 - 可行性
算法的每一步都是可行的
效率度量
1.事後統計法
比較不同算法對同一組輸入數據的運行處理時間
缺陷
需要編寫程序,依賴硬件,測試數據
2.算法效率度量
依據統計的方法對算法效率進行估算
影響算法效率的主要因素:
- 算法的策略與方法
- 問題規模
- 編譯器
- 計算機執行速度
- 判斷一個算法的效率時,往往只需關注操作數量的最高次項,其他次要項和常數項,可以忽略
- 時間複雜度指最壞時間複雜度
大O表示法
算法效率嚴重依賴於操作(Operation)數量
在判斷時首先關注操作數量的最高次項
操作數量的估算可以作爲時間複雜度的估算
O(5) = O(1)
O(2n + 1) = O(2n) = O(n)
O(n2+ n + 1) = O(n2)
O(3n3+1) = O(3n3) = O(n3)
常見的複雜度
時間複雜度大小關係。
算法的空間複雜度。
算法的空間複雜度通過計算存儲空間實現
S(n)=O(f(n))
其中n 爲問題規模,f(n)爲在問題規模爲n時所佔用存儲空間的函數,大O
表示法同樣適用於算法的空間複雜度,當算法執行時所需要空間複雜度是常數時候,空間複雜度是O(1)
空間與時間的策略
多數情況下,算法執行時所用的時間更令人關注
如果有必要,可以通過增加空間複雜度來降低時間複雜度
同理,也可以通過增加時間複雜度來降低空間複雜度
數據結構詳細
線性表
- 線性表(List)是零個或多個數據元素的集合
- 線性表中的數據元素之間是有順序的
- 線性表中的數據元素個數是有限的
- 線性表中的數據元素的類型必須相同
線性表的基本操作
- 創建線性表
- 銷燬線性表
- 清空線性表
- 將元素插入線性表
- 將元素從線性表中刪除
- 獲取線性表中某個位置的元素
- 獲取線性表的長度
線性表的順序存儲
線性表的問題
優點:無需爲線性表中的邏輯關係增加額外的空間
可以快速的獲取表中合法位置的元素
缺點:
插入和刪除操作需要移動大量元素
當線性表長度變化較大時,難以確定存儲空間的容量
線性表的鏈式存儲
爲了表示每個元素與其後繼元素的邏輯關係,每個元素除了存儲本身的信息之外,還需要存儲指示其直接後繼的信息。
鏈表的技術推演。
循環鏈表
循環鏈表的基本操作。
- 創建鏈表
- 銷燬鏈表
- 獲取長度
- 清空
- 獲取指定位置的元素
- 插入到指定位置
- 刪除指定位置
新增的功能:
遊標,用來指向當前的節點,通常可以用來遍歷用。
基本結構
特殊問題
循環鏈表插入場景分析
循環鏈表的刪除場景
雙向鏈表
爲什麼需要雙向鏈表,單鏈表只有一個指向下個節點的指針,無法直接訪問其前驅指針,逆序訪問過分浪費時間。
基本定義
在單鏈表的結點中增加一個指向其前驅的 pre 指針
技術場景
插入場景
刪除場景
棧 和 隊列
棧是一種 特殊的線性表
棧僅能在線性表的一端進行操作 棧頂(Top):允許操作的一端
棧底(Bottom):不允許操作的一端
一般的,我們建議使用鏈式的線性表作爲棧的底層結構,每次入棧,往線性表的頭號元素插入數據,每次出棧,從頭號元素pop.這樣子,就不需要用到末尾的節點,從而提高入棧和出棧的效率
當時,如果我們是採用順序存儲的線性表,即底層是數組的方式?那麼應該是在數組尾部作爲出入棧的效率高一些
有一點需要注意的是,用戶入棧往往是一個void* 指針,那麼我們並無法要求用戶實現我們的類,我們只好在棧的入棧加一層結構,以迎合底層需要的數據結構要求。
隊列
隊列的模型
隊列的存儲與實現
隊列也是一種特殊的線性表,可以用線性表的順序存儲來模擬
順序存儲基本API
#ifndef _MY_SEQQUEUE_H_ #define _MY_SEQQUEUE_H_
typedef void SeqQueue;
SeqQueue* SeqQueue_Create(int capacity);
void SeqQueue_Destroy(SeqQueue* queue);
void SeqQueue_Clear(SeqQueue* queue);
int SeqQueue_Append(SeqQueue* queue, void* item); void* SeqQueue_Retrieve(SeqQueue* queue);
void* SeqQueue_Header(SeqQueue* queue);
int SeqQueue_Length(SeqQueue* queue);
int SeqQueue_Capacity(SeqQueue* queue);
#endif //_MY_SEQQUEUE_H_
隊列同時也可以用鏈式進行存儲,
由於隊列是一端進入,另外一端輸出,所以,在操作的時候,無論是順序存儲的線性表,還是鏈式存儲的線性表,作爲其底層的結構,在入列和出列的時候,其至少有一種操作需要遍歷整的一個隊列的。所以O(n)的時間複雜度是很難去避免的。
樹
應該如何存儲?順序存儲,復原不容易,鏈式存儲,一個前驅,n個後繼,後繼難以確定。所以用二叉樹是一個比較好的選擇。
怎麼把一棵普通的樹轉化成爲二叉樹?可以採用左孩子,右兄弟的方式
轉化成如下。
節點的結構
三個性質
- 性質1: 在二叉樹的第i層上至多有2i-1個結點(i>0)
- 性質2: 深度爲k的二叉樹至多有2k-1個結點(k>0)
- 性質3: 對於任何一棵二叉樹,若2度的結點數有n2個,則葉子數(n0)必定爲n2+1 (即n0=n2+1)
- 性質4: 具有n個結點的完全二叉樹的深度必爲log2n+1
- 性質5: 對完全二叉樹,若從上至下、從左至右編號,則編號爲i 的結點,其左孩子編號必爲2i,其右孩子編號必爲2i+1;其雙親的編號必爲i/2(i=1 時爲根,除外)
滿二叉樹
完全二叉樹
二叉樹的表示
二叉鏈表示法
一般從根節點開始存儲,響應的,訪問樹種節點時,也只能從根節點開始。
數據節點內容
typedef struct BiTNode
{
int data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
三叉鏈表表示法
每一個基點都有3個指針
//三叉鏈表
typedef struct TriTNode
{
int data;
//左右孩子指針
struct TriTNode *lchild, *rchild;
struct TriTNode *parent;
}TriTNode, *TriTree;
雙親表示法
存儲結構
如圖,即每一個節點都是由一個數據結構組成,結構中,有該節點的父節點所在數組的索引。
//雙親鏈表
#define MAX_TREE_SIZE 100
typedef struct BPTNode
{
int data; // 數據
int parentPosition; //指向雙親的指針,數組下標
char LRTag; //左右孩子標誌域
}BPTNode;
typedef struct BPTree
{
//因爲節點之間是分散的,需要把節點存儲到數組中
BPTNode nodes[100];
int num_node; //節點數目
//根結點的位置,注意此域存儲的是父親節點在數組的下標
int root;
}BPTree;
二叉樹的遍歷
樹的遍歷一共有3種方案
樹的非遞歸遍歷
二叉樹的創建
通過中序遍歷和先序遍歷可以確定一棵樹
通過中序遍歷和後序遍歷可以確定一棵樹
但是通過先序遍歷和後序遍歷是無法確定一棵樹
#號法創建樹
#創建樹,讓樹的每一個節點都變成度爲2的的樹
先序遍歷:1 2 4 # # # 3 # # 可以唯一確定一棵樹嗎,爲什麼?
排序算法詳細
選擇排序
選擇排序,即第一遍,以第一個元素爲基數,與後面的比較,一旦發現後面更小的數字,把更小的數字放置到第一個元素的位置,這樣,一趟下來,一號元素就是最小的數字。
第二遍,從第二個元素開始。使得第二個元素是最小,如此一來一遍又一遍。
第一遍,遍歷n個項目,比較n-1次
第二遍,遍歷n-1項目,比較n-2次
…
第n-1遍,遍歷2項目,比較1次
總的比較次數是
Sn=(n-1)+(n-2)+(n-3)…+1=n*(n-1)/2=O(n2)
冒泡排序
冒泡排序如下圖,從右邊往左邊遍歷,一旦左邊比右邊大,交換。每次拿兩個項目進行比較交換。
第一趟:n-1次比較
第二趟: n-2次比較
第n-1趟: 1 次比較
算法複雜度 同上,代碼更加複雜而已。
總共有2種不同的冒泡方式,一種沉入水底,一種浮到水面
插入排序
如圖片所示,按照順序拿一個數字出來,這個數字與他前面已經有序的進行比較,插入前面有序隊列中對應合適的位置。
第一次是49,左邊沒有數據,直接入列
第二次是38 ,與左邊列(49)比較,知道其應該放在49左邊,隊列此時有序
第三次是65,與左邊隊列比較得知應該在49右邊,此時(38 49 65)
以此類推
第n 次27 ,依次比較,知道發現13 38之間,將其插入。
由於算法複雜度度量的是最壞情況下算法耗費的時間,所以,
第一趟,0次比較
第二趟, 1次比較
第三趟,2次比較
…
第n趟,n-1次比較
那麼最壞的情況下依然是O(n2)。
比起上面兩種,在某種意義上是快了一丟丟。比如隊列本來就是已經排序完畢了。哈哈。有點搞笑哈。我們算法複雜度不跟你搞這些花裏胡哨的。O(n2)直接淘汰。但是在某種情況下,插入排序效率卻是很高,假設如果我們的序列剛好是有序的,即每次與序列中的第一個元素對比,就能夠知道其優先級,那麼只需要比較以此就夠了。
代碼實現的時候應該把被選中元素緩存,並且把這個元素位置放空,然後與有序序列進行比對,一旦比對到比他小的位置,那麼這個位置之前比較的全部往後面移動,讓出來給這個元素。
希爾排序
1.先將待排元素分割成若干個子序列,對子序列進行插入排序。使得子序列有序
一般子序列的跨度爲gap=gap/3+1;保證序列中元素足夠少,而且大概有序。
/**
* @brief shellSort 希爾排序
* @param arr
* @param len
* @return
*/
int shellSort(int* arr,int len){
int gap=len;
do{
//分組大小爲gap大小
gap=gap/3+1;//O(n*1.3)
//決定一共有多少個分組
for(int k=0;k<gap;++k){
//開始最表爲xxx的分組,以間隙爲xxx
printf("start idx:%d groups with gap:%d\n",k,gap);
//他會是這樣的
printf("and it would be like:");
for(int x=k;x<len;x+=gap){
printf("%d ",x);
}
// 0 1 2 3 4 5 6 7
for(int i=k+gap;i<len;i+=gap){
printf("\n%d ",i);
int tmp=arr[i];
int j=i-gap;
// 0 1 2 [0.4] 4 5
printf("\nprevious was:");
for(;j>=k&&(arr[j]>tmp);j-=gap){
printf("%d ,",j);
printf("\nprev val:%d currentVal:%d",arr[j],tmp);
arr[j+gap]=arr[j];
}
arr[j+gap]=tmp;
}
printf("\n");
}
}while(gap>1);
for(int i=0;i<len;i++){
printf("%d ",arr[i]);
}
return 0;
}
快速排序
//快速排序
void quickSort(int s[], int l, int r)
{
if (l < r)
{
int i = l, j = r;
// 拿出第一個元素, 保存到x中,第一個位置成爲一個坑
int x = s[l];
while (i < j)
{
// 從右向左找小於x的數
while (i < j && s[j] >= x)
{
//左移, 直到遇到小於等於x的數
j--;
}
if (i < j)
{
//將右側找到的小於x的元素放入左側坑中,
//右側出現一個坑,左側元素索引後移
s[i++] = s[j];
}
// 從左向右找大於等於x的數
while (i < j && s[i] < x)
{
//右移, 直到遇到大於x的數
i++;
}
if (i < j)
{
//將左側找到的元素放入右側坑中, 左側出現一個坑
//右側元素索引向前移動
s[j--] = s[i];
}
}
//此時 i=j,將保存在x中的數填入坑中
s[i] = x;
quickSort(s, l, i - 1); // 遞歸調用
quickSort(s, i + 1, r);
}
}
歸併排序
假定A,B是有序的序列,那麼遍歷A,遍歷B,A元素與B元素比較,比較小的那個入到隊列中。
歸併步驟如下。
其次,我們的問題是,爲啥A,B中元素是有序的呢?那麼我們假設A,B都是隻有一個元素的序列,那他們就是有序 的,歸併後的也是有序的。所以可以通過把一個數組不斷的拆分,拆分到最後,只有一個元素,讓後我們再執行歸併。
/**
* @brief merge 歸併排序
* @param a 基礎數組
* @param first 第一號元素
* @param mid 中位元素
* @param last 末尾元素
* @param temp 臨時數組
* @return
*/
int merge(int a[],int first,int mid,int last,int temp[]){
int leftStart=first;
int leftEnd=mid;
int rightStart=mid+1;
int rightEnd=last;
int length=0;
int i=leftStart;
int j=rightStart;
while (i<=leftEnd&&j<=rightEnd) {
if(a[i]<a[j]){
temp[length++]=a[i++];
}else{
temp[length++]=a[j++];
}
}
while(i<=leftEnd){
temp[length++]=a[i++];
}
while(j<=rightEnd){
temp[length ++]=a[j++];
}
//最後,還需要將臨時中已經拍好序列的拷貝到a中,讓a對應部分稱爲有序的序列。
for(int i=0;i<length;i++){
a[leftStart+i]=temp[i];
}
return 0;
}
/**
* @brief mergeSort 歸併排序
* @param arr
* @param len
* @return
*/
int mergeSort(int *arr,int first,int last,int temp[]){
if(first<last){
int mid=(last+first)/2;
mergeSort(arr,first,mid,temp);
mergeSort(arr,mid+1,last,temp);
merge(arr,first,mid,last,temp);
}
return 0;
}
/**
* @brief mergeSortExt 歸併排序的有點
* 複雜度O(N*logN) 是一種穩定的排序算法
* @param arr
* @param size
* @return
*/
int mergeSortExt(int* arr,int size){
int temp[size];
mergeSort(arr,0,size-1,temp);
for(int i=0;i<size;i++){
printf("%d ",arr[i]);
}
}
排序算法總結
希爾排序看似乎挺複雜,也有着不穩定的特性,但是他可以不用涉及到遞歸,這也就意味着,對於超長的序列,他的排序可以達到棧不溢出的目的,但是快排與之相比是不行的,快排依賴遞歸,而且是一種不穩定的算法,好處在於代碼容易理解。歸併排序雖然穩定,但是其也是遞歸的。所以針對後面三種突破O(n2)的算法,建議讀者有時間一定要好好的研究。至少對着代碼自己手敲一遍。這也是對於作爲程序員最最基本的要求。