1. 程式人生 > 實用技巧 >MySQL鎖(三)行鎖:幻讀是什麼?如何解決幻讀?

MySQL鎖(三)行鎖:幻讀是什麼?如何解決幻讀?

技術標籤:資料結構資料結構連結串列指標

簡單資料結構的妙用(一):靜態連結串列

目錄

簡單資料結構的妙用(一):靜態連結串列

一、參考文章

二、靜態連結串列的背景

三、基本實現

四、插入操作

五、刪除操作

六、連結串列長度

七、優缺點


一、參考文章

內容主要出自程傑的《大話資料結構》靜態連結串列一節。

  1. “指標”是成熟的程式語言必須具有的概念嗎?—— 程式碼宇宙的回答:https://segmentfault.com/q/1010000003797714/a-1020000003798889

二、靜態連結串列的背景

指標的本意:在一個變數中儲存另一個變數的地址,以提供將”地址“變數化的能力。如果沒有指標,將無法用一個變數引用另一個變數(只能把變數的值拷貝一份賦給另一個變數)。

C語言提供了完善的指標操作,包括:為指標賦值、記憶體分配(malloc)、取變數地址、讓指標可以參與運算等,這使得C程式設計師能夠任意操作可用記憶體。

Java、C#等雖不使用指標,但因為啟用了物件引用機制,從某種角度也間接實現了指標的某些作用。

但如Basic、Fortran等早期的程式語言是沒有指標的,也沒有物件引用機制,如此,便無法實現很多基本的資料結構,如連結串列、樹、圖等,這些資料結構都必須用指標來儲存前驅或後繼節點的地址。

那怎麼辦呢?

使用陣列來代替指標,描述單鏈表。

雖然現在大部分語言都可以靠指標或物件引用機制來實現連結串列,這種用資料代替指標描述單鏈表的思想是很獨到的,這種用資料描述的連結串列叫靜態連結串列。思想很簡單,實現也很簡單,但也確實很巧妙。

三、基本實現

首先,單鏈表的每個結點都包含要處理的資料和指向後繼結點的指標,那麼對應陣列的元素即為data和cur。data就是要處理的資料,cur相當於next指標,cur也叫遊標(Cursor)。

為了方便插入資料,通常把陣列建立得大一些,以便有一些空閒空間可以便於插入時不至於溢位。

線性表得靜態連結串列儲存結構的程式碼如下:

#define MAXSIZE 1000  // 假設連結串列的最大長度是1000
typedef struct {
  ElemType data; // 資料
  int cur;    // 遊標
}Component, StaticLinkList[MAXSIZE];

對陣列的第一個和最後一個元素作特殊處理,不存資料。

通常把未被使用的陣列元素稱為備用連結串列。

陣列的第一個元素,即下標為0的元素的cur,就存放備用連結串列的第一個結點的下標;陣列的最後一個元素的cur,存放第一個有數值的元素的下標,相當於單鏈表中頭結點的作用,當整個連結串列為空時,其為0。如圖所示:

初始化陣列的程式碼如下:

// 將一維陣列space中各分量鏈成一備用連結串列,space[0].cur是頭指標
Bool InitList(StaticLinkList space) {
  for(int i = 0; i < MAXSIZE - 1; i++) {
    space[i].cur = i + 1;
   }
  space[MAXSIZE - 1].cur = 0; // 目前靜態連結串列為空,最後一個元素的cur為0 
  return true;
}

假設將資料存入靜態連結串列,比如分別存放著"甲”、“乙”、“丁”、“戊”、“己”、“庚"等資料,如圖所示:

此時,”甲“存有下一元素”乙“的遊標2,”乙“存有下一元素”丁“的下標3,”庚“是最後一個有值的元素,所以它的cur是0。最後一個元素的cur因為”甲“是第一個有值的元素而存它的下標1。第一個元素因空閒空間的第一個元素下標為7,所以它的cur存7。

四、插入操作

靜態連結串列中要解決的是:如何用靜態模擬動態連結串列結構的儲存空間的分配,需要時申請,無用時釋放。

在動態連結串列中,結點的申請和釋放分別借用malloc()和free()兩個函式來實現。在靜態連結串列中,操作的是陣列,不存在像動態連結串列的結點申請和釋放問題,所以需要自己實現這兩個函式。

為辨明陣列中哪些分量未被使用,解決的辦法是將所有未被使用過的及已被刪除的分量用遊標鏈成一個備用的連結串列,每當進行插入時,便可以從備用連結串列上 取得第一個結點作為待插入的新結點。

節點申請的程式碼如下:

// 若備用連結串列非空,則返回分配的結點下標,否則返回0
int Malloc_SLL(StaticLinkList space) {
    int i = space[0].cur; // 當前陣列第一個元素cur的值,就是要返回的備用連結串列的第一個元素下標
  if (space[0].cur) {
    space[0].cur = space[i].cur; // 由於拿出一個分量來使用了,就得把它的下一個分量用來做備用
   }
  return i;
}

現在如果需要在”乙”和“丁“之間插入一個值為”丙“的元素,按照以前順序儲存結構的做法,應該要把“丁”、“戊”、“己”、“庚"這些元素都往後移一位,但目前不用,因為有了新的手段。

新元素”丙“想插隊是吧?可以,讓它先排在隊伍最後第7個遊標的位置,然後,將“乙”原先的cur是遊標為3的“丁”,現在改成7,再將“丙”的cur設定為3即可。這樣,在絕大多數元素都沒有變動的情況下,整個連結串列的次序發生了改變,如圖所示:

插入的程式碼如下:

// 在L中第i個元素之前插入新的元素e
Bool ListInsert(StaticLinkList L, int i, ElemType e) {
    if (i < 1 || i > ListLength(L) + 1) {
        return false;    
   }
  int j, k, l;
  k = MAX_SIZE - 1;  // 注意k首先是最後一個元素的下標
    j = Malloc_SLL(L); // 獲得空閒分量的下標
  if (!j) {
    return false;
   }
  L[j].data = e;   // 將資料賦值給此分量的data
  for (l = 1; l < i; l++) { // 找到第i個元素之前的位置
    k = L[k].cur;
   }
  L[j].cur = L[k].cur; // 把第i個元素之前的cur賦值給新元素的cur
  L[k].cur = j;     // 把新元素的下標賦值給第i個元素之前元素的cur
  return true;
}

這樣,就實現了在陣列中,不移動元素,卻插入了資料的操作。

五、刪除操作

和前面一樣,刪除元素時,是需要釋放結點的函式free()。現在也需自己實現:

// 刪除在L中第i個數據元素e
Bool ListDelete(StaticLinkList L, int i) {
  if (i < 1 || i > ListLength(L)) {
    return false;
   }
  int j, k;
  k = MAX_SIZE - 1;
  for (j = 1; j < i; j++) {
    k = L[k].cur;
   }
  j = L[k].cur;
  L[k].cur = L[j].cur;
  Free_SSL(L, j);
  return true;
}
// 將下標為k的空閒結點回收到備用連結串列
void Free_SLL(StaticLinkList L, int i) {
  L[i].cur = space[0].cur; // 把第一個元素cur值賦給要刪除的分量cur
  L[0].cur = i;       // 把要刪除的分量下標賦值給第一個元素的cur
}

六、連結串列長度

靜態連結串列的ListLength實現:

// 初始條件:靜態連結串列L已存在。操作結果:返回L中資料元素個數
int ListLength(StaticLinkList L) {
  int j = 0, i = L[MAX_SIZE - 1].cur;
  while (i) {
    i = L[i].cur;
    j++;
   }
  return j;
}

七、優缺點

  1. 優點:在插入和刪除操作時,只需要修改遊標,不需要移動元素,從而改進了在順序儲存結構中的插入和刪除操作需要移動大量元素的缺點。

  2. 缺點:

    • 沒有解決連續儲存分配帶來的表長難以確定的問題(必須事先知道元素個數來分配記憶體,單鏈表則無需如此);

    • 失去了順序儲存結構隨機存取的特性(原先通過下標可直接存取);