數據結構與算法之線性表算法練習

前言

前面瞭解學習了線性表的單向鏈表和單線循環鏈表和雙向鏈表的一些知識,本篇搞幾個算法題實戰一下。首先,做下面準備代碼:

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

// 存儲空間初始分配量
#define MAXSIZE 20

typedef int Status;/* Status是函數的類型,其值是函數結果狀態代碼,如OK等 */
typedef int ElemType;/* ElemType類型根據實際情況而定,這裏假設爲int */

// 定義節點
typedef struct Node {
    ElemType data;
    struct Node *next;
}Node;

typedef struct Node *LinkList;

// 初始化鏈表
Status InitList(LinkList *L) {
 
    // 創建頭節點,L指向頭結點
    *L = (LinkList)malloc(sizeof(Node));
    //存儲空間分配失敗
    if (*L == NULL) {
        return ERROR;
    }
    //將頭結點的指針域置空
    (*L)->next = NULL;
    
    return OK;
}

// 插入
Status ListInsert(LinkList *L,int i,ElemType e){
    int j;
    LinkList p,s;
    p = *L;
    j = 1;
    
    //尋找第i個結點
    while (p && j<i) {
        p = p->next;
        ++j;
    }
    
    //第i個元素不存在
    if(!p || j>i) return ERROR;
    
    //生成新結點s
    s = (LinkList)malloc(sizeof(Node));
    //將e賦值給s的數值域
    s->data = e;
    //將p的後繼結點賦值給s的後繼
    s->next = p->next;
    //將s賦值給p的後繼
    p->next = s;
    
    return OK;
}

// 輸出鏈表
Status showList(LinkList L) {
    LinkList p = L->next;
    
    while (p) {
        printf("%d  ",p->data);
        p = p->next;
    }
    printf("\n");
    return OK;
}

題目一

   將2個遞增的有序鏈表合併爲一個有序鏈表;要求結果鏈表仍然使用兩個鏈表的存儲空間,不另外佔用其他的存儲空間。表中不允許有重複的數據。

關鍵詞:

有序遞增鏈表,不允許有重複數據,保留遞增關係(後插法)
不佔用額外的存儲空間指的是不能開闢新節點,賦值在鏈接到鏈表上;

思路:

1. 假設待合併的鏈表爲 La 和 Lb,合併後的新表使用頭指針 Lc(Lc的表頭結點設爲La的表頭結點)指向.
   Pa 和 Pb 分別是La,Lb的工作指針.初始化爲相應鏈表的首元結點
2. 從首元結點開始比較, 當兩個鏈表 La 和 Lb,均未到達表尾結點時,依次摘取其中較小值重新鏈表在
   Lc 表的最後.
3. 如果兩個表中的元素相等,只摘取La表中的元素,刪除Lb表中的元素,確保無重複;
4. 當一個表達到表尾結點爲空時,非空表的剩餘元素直接鏈接在Lc表最後.
5. 最後釋放鏈表Lb的頭結點;
void MergeList(LinkList *La, LinkList *Lb, LinkList *Lc){
    
    //將2個遞增的有序鏈表La,Lb 合併爲一個遞增的有序鏈表Lc
    LinkList pa, pb, pc, temp;
    // 將pa、pb分別初始化爲對應的首元結點
    pa = (*La)->next;
    pb = (*Lb)->next;
    
    *Lc = pc = *La;
    while (pa && pb) {
        if (pa->data < pb->data) {
            // 取較小的元素,將pa鏈接在pc後面,pa後移
            pc->next = pa;
            pc = pc->next;
            pa = pa->next;
        } else if (pa->data == pb->data){
            // 取pa元素,刪除pb,pa和pb後移
            pc->next = pa;
            pc = pc->next;
            pa = pa->next;
            
            temp = pb;
            pb = pb->next;
            free(temp);
        }else {
            pc->next =pb;
            pc = pc->next;
            pb = pb->next;
        }
    }
    
    //將非空表的剩餘元素之間鏈接在Lc表的最後
    pc->next = pa ? pa : pb;
    
    //釋放Lb的頭結點
    free(*Lb);
}
// 調用
        //設計2個遞增鏈表La,Lb
        for(int j = 10;j>=0;j-=2)
        {
            iStatus = ListInsert(&La, 1, j);
        }
        printf("La:\n");
        showList(La);

        for(int j = 11;j>0;j-=2)
        {
            iStatus = ListInsert(&Lb, 1, j);
        }
        printf("Lb:\n");
        showList(Lb);

        MergeList(&La, &Lb, &Lc);
        printf("Lc:\n");
        showList(Lc);

題目二

   已知兩個鏈表A和B分別表示兩個集合.其元素遞增排列. 設計一個算法,用於求出A與B的交集,並存儲在A鏈表中;

例如: La = {2,4,6,8};Lb = {4,6,8,10};
Lc = {4,6,8}

關鍵詞:

依次摘取2個表中相等的元素重新進行鏈接,刪除其他不等的元素

思路:

1. 合併的鏈表爲La和Lb,合併後的新表使用頭指針Lc(Lc的表頭結點設爲La的表頭結點)指向. 
  Pa 和 Pb 分別是La,Lb的工作指針.初始化爲相應鏈表的首元結點
2. 從首元結點開始比較,當兩個鏈表La 和Lb 均未到達表尾結點時
3. 如果兩個表中的元素相等,只摘取La表中的元素,鏈接到Lc後面,刪除Lb表中的元素
4. 如果其中一個表中的元素較小,刪除此表中較小的元素. 此表的工作指針後移
5. 當鏈表 La和Lb有一個先到達表尾結點爲空時,依次刪除另一個非空表中的所有元素,最後釋放鏈表lb

代碼:

void Intersection(LinkList *La, LinkList *Lb, LinkList *Lc){
    //2個遞增的有序鏈表La,Lb的交集, 使用頭指針Lc指向帶回;
    LinkList pa, pb, pc, temp;
    
    // 將pa、pb分別初始化爲對應的首元結點
    pa = (*La)->next;
    pb = (*Lb)->next;
    
    *Lc = pc = *La;
    while (pa && pb) {
        if (pa->data == pb->data){
            // 取pa元素,刪除pb,pa和pb後移
            pc->next = pa;
            pc = pc->next;
            pa = pa->next;
            
            temp = pb;
            pb = pb->next;
            free(temp);
        }else if (pa->data < pb->data) {
            // 刪除較小值La的元素;,pa後移
            temp = pa;
            pa = pa->next;
            free(temp);

        } else {
            // 刪除較小值La的元素;,pb後移
            temp = pb;
            pb = pb->next;
            free(temp);
        }
    }
    
    //Lb爲空,刪除非空表La中的所有元素
    while (pa) {
       
        temp = pa;
        pa = pa->next;
        free(temp);
    }
    
    //La爲空,刪除非空表Lb中的所有元素
    while (pb) {
        temp = pb;
        pb = pb->next;
        free(temp);
    }
    
    // 將尾結點pc的next指向NULL
    pc->next = NULL;
    free(*Lb);
    
}

//調用
        ListInsert(&La, 1, 8);
        ListInsert(&La, 1, 6);
        ListInsert(&La, 1, 4);
        ListInsert(&La, 1, 2);
        printf("La:\n");
        showList(La);


        ListInsert(&Lb, 1, 10);
        ListInsert(&Lb, 1, 8);
        ListInsert(&Lb, 1, 6);
        ListInsert(&Lb, 1, 4);
        printf("Lb:\n");
        showList(Lb);

        Intersection(&La, &Lb, &Lc);
        printf("Lc:\n");
        showList(Lc);

題目三

  設計一個算法,將鏈表中所有節點的鏈接方向"原地旋轉",即要求僅僅利用原表的存儲空間. 換句話說,要求算法空間複雜度爲O(1);

例如:L = {0,2,4,6,8,10},逆轉後:L = {10,8,6,4,2,0};

關鍵詞:

1. 不能開闢新的空間,只能改變指針的指向;
2. 考慮逐個摘取結點,利用前插法創建鏈表的思想,將結點一次插入到頭結點的後面; 
   因爲先插入的結點爲表尾,後插入的結點爲表頭,即可實現鏈表的逆轉;

思路:

1. 利用原有的頭結點*L,p爲工作指針, 初始時p指向首元結點. 因爲摘取的結點依次向前插入,爲確保鏈表
   尾部爲空,初始時將頭結點的指針域置空;
2. 從前向後遍歷鏈表,依次摘取結點,在摘取結點前需要用指針q記錄後繼結點,以防止鏈接後丟失後繼結點;
3. 將摘取的結點插入到頭結點之後,最後p指向新的待處理節點q(p=q);

代碼:

void Inverse(LinkList *L){
    // 利用頭插法,逆轉帶頭結點單鏈表L
    LinkList p,q;
    
    //p指向首元結點;
    p = (*L)->next;
    
    //頭結點的指針域置空
    (*L)->next = NULL;
    
    while (p!=NULL) {
        q = p->next;
        // p的next 指向頭結點next
        p->next = (*L)->next;
        //*p 插入到頭結點之後;
        (*L)->next = p;
        //處理下一個結點
        p = q;
    }
}

// 調用
        InitList(&L);
        for(int j = 10;j>=0;j-=2)
        {
            iStatus = ListInsert(&L, 1, j);
        }
        printf("L逆轉前:\n");
        showList(L);

        Inverse(&L);
        printf("L逆轉後:\n");
        showList(L);

題目四

  設計一個算法,刪除遞增有序鏈表中值大於等於mink且小於等於maxk(mink,maxk是給定的兩個參數,其值可以和表中的元素相同,也可以不同)的所有元素

關鍵詞:

通過遍歷鏈表能夠定位帶刪除元素的下邊界和上邊界, 即可找到第一個值大於mink的結點和第一個值大於等於maxk的結點;

算法思想:

1. 查找第一個值大於mink的結點,用q指向該結點,pre 指向該結點的前驅結點;
2. 繼續向下遍歷鏈表, 查找第一個值大於等於maxk的結點,用p指向該結點;
3. 修改下邊界前驅結點的指針域, 是其指向上邊界(pre->next = p);
4. 依次釋放待刪除結點的空間(介於pre和p之間的所有結點)

代碼:

void DeleteMinMax(LinkList *L, int mink, int maxk){
    LinkList p,q,pre;
    
    p = (*L)->next;
    pre = *L;
    
    LinkList temp;
    
    // 查找第一值大於mink的結點
    while (p && p->data < mink) {
        pre = p;
        p = p->next;
    }

    //查找第一個值大於等於maxk的結點
    while (p && p->data < maxk) {
        p = p->next;
    }
    
    //修改待刪除的結點指針
    q = pre->next;
    pre->next = p;
    
    // 刪除mink到maxk之間的元素
    while (q != p) {
        temp = q->next;
        free(q);
        q = temp;
    }
    
}

// 調用
        InitList(&L);
        for(int j = 10;j>=0;j-=2)
        {
            iStatus = ListInsert(&L, 1, j);
        }
        printf("L鏈表:\n");
        showList(L);

        DeleteMinMax(&L, 4, 10);
        printf("刪除鏈表mink與maxk之間結點的鏈表:\n");
        showList(L);

題目五

   設將n(n>1)個整數存放到一維數組R中, 試設計一個在時間和空間兩方面都儘可能高效的算法;將R中保存的序列循環左移p個位置(0<p<n)個位置, 即將R中的數據由(x0,x1,…,xn-1)變換爲(xp,xp+1,…,xn-1,x0,x1,…,xp-1).

例如:

  pre[10] = {0,1,2,3,4,5,6,7,8,9},
  n = 10,p = 3;
  pre[10] = {3,4,5,6,7,8,9,0,1,2}

思路:

1. 先將n個數據原地逆置 9,8,7,6,5,4,3,2,1,0;
2. 將n個數據拆解成[9,8,7,6,5,4,3] [2,1,0]
3. 將前 n-p 個數據和後 p 個數據分別原地逆置; [3,4,5,6,7,8,9] [0,1,2]

複雜度分析:
時間複雜度: O(n); 時間複雜度:O(1);

代碼:

// 翻轉
void Reverse(int *pre,int left ,int right){
    
    //將數組R中的數據原地逆置
    
    //i等於左邊界left,j等於右邊界right;
    int i = left,j = right;
    int temp;
    
    //交換pre[i] 和 pre[j] 的值
    while (i < j) {
        
        //交換
        temp = pre[i];
        pre[i] = pre[j];
        pre[j] = temp;
        
        //i右移,j左移
        i++;
        j--;
    }
}

void LeftShift(int *pre,int n,int p){
    
    //將長度爲n的數組pre 中的數據循環左移p個位置
    if (p>0 && p<n) {
        //1. 將數組中所有元素全部逆置
        Reverse(pre, 0, n-1);
        //2. 將前n-p個數據逆置
        Reverse(pre, 0, n-p-1);
        //3. 將後p個數據逆置
        Reverse(pre, n-p, n-1);
        
    }
}

// 調用
int pre[10] = {0,1,2,3,4,5,6,7,8,9};
LeftShift(pre, 10, 3);
for (int i=0; i < 10; i++) {
    printf("%d ",pre[i]);
}

題目六

已知一個整數序列 A = (a0,a1,a2,...an-1),其中(0<= ai <=n),(0<= i<=n), 若存在ap1= ap2 = ...= apm = x,且m>n/2(0<=pk<n,1<=k<=m)則稱xA的主元素

例如:

A = (0,5,5,3,5,7,5,5),則5是主元素; 
若B = (0,5,5,3,5,1,5,7),則B中沒有主元素,

假設A中的n個元素保存在一個一維數組中,請設計一個儘可能高效的算法,找出數組元素中的主元素,若存在主元素則輸出該元素,否則輸出-1。

思路:

1. 選取候選主元素, 從前向後依次掃描數組中的每個整數, 假定第一個整數爲主元素,將其保存在Key中,
   計數爲1
2. 若遇到下一個整數仍然等於key,則計數加1, 否則計數減1。
3. 當計數減到0時,將遇到的下一個整數保存到key中, 計數重新記爲1. 開始新一輪計數.
   即可從當前位置開始重上述過程,直到將全部數組元素掃描一遍;
4. 判斷key中的元素是否是真正的主元素, 再次掃描數組, 統計key中元素出現的次數,
   若大於n/2,則爲主元素,否則,序列中不存在主元素;


算法分析:
時間複雜度: O(n)
空間複雜度: O(1)
int MainElement(int *A, int n){

    // count 用來計數
    int count = 1;
    // key 用來保存候選主元素, 初始A[0]
    int key = A[0];
    
    //掃描數組,選取候選主元素
    for (int i = 1; i < n; i++) {
        
        // 如果A[i]元素值 == key ,則候選主元素計數加1;
        if (A[i] == key) {
            count++;
        }else{
            //3. 當前元素A[i] 非候選主元素,計數減1;
            if(count >0){
                count--;
                
            }else{
               // 如果count 等於0,則更換候選主元素,重新計數
                key = A[i];
                count = 1;
            }
            
        }
    }

    //如果count >0
    if (count >0){
        
        // 統計候選主元素的實際出現次數
        for (int i = count = 0; i < n; i++)
            if (A[i] == key) count++;
    }
    
    // 判斷count>n/2, 確認key是不是主元素
    if (count > n/2) return key;
    else return -1; //不存在主元素

}

// 調用
        int  A[] = {0,5,5,3,5,7,5,5};
        int  B[] = {0,5,4,3,0,0,0,0};
        int  C[] = {0,1,2,3,4,5,6,7};

        int value = MainElement(A, 8);
        printf("數組A 主元素爲: %d\n",value);
        value = MainElement(B, 8);
        printf("數組B 主元素爲(-1表示數組沒有主元素): %d\n",value);
        value = MainElement(C, 8);
        printf("數組C 主元素爲(-1表示數組沒有主元素): %d\n",value);

題目七

   用單鏈表保存m個整數, 結點的結構爲(data,link),且|data|<=n(n爲正整數)。 現在要去設計一個時間複雜度儘可能高效的算法,對於鏈表中的data 絕對值相等的結點,僅保留第一次出現的結點,而刪除其餘絕對值相等的結點。

例如:

鏈表 A = {21,-15,15,-7,15}, 刪除後的鏈表 A = {21,-15,-7};

分析:

設計一個時間複雜度儘量高效的算法,而已知|data|<=n, 所以可以考慮用空間換時間的方法.

思路:

1. 申請大小爲 n+1 的輔助數組 t 並賦值初值爲0
2. 從首元結點開始遍歷鏈表,依次檢查t[|data|]的值
3. 若[|data|]爲0,即結點首次出現,則保留該結點,並置t[|data|] = 1,若t[|data|]不爲0,
   則將該結點從鏈表中刪除.

複雜度分析:
時間複雜度: O(m),對長度爲m的鏈表進行一趟遍歷,則算法時間複雜度爲O(m);
空間複雜度: O(n)

代碼:

void DeleteEqualNode(LinkList *L,int n){
    
    //1. 開闢輔助數組p.
    int *p = alloca(sizeof(int)*n);
    // r記錄當前節點,
    LinkList r = *L;
    
    //2.數組元素初始值置空
    for (int i = 0; i < n; i++) {
        *(p+i) = 0;
    }
    //3.指針temp 指向首元結點
    LinkList temp = (*L)->next;
    
    //4.遍歷鏈表,直到temp = NULL;
       while (temp!= NULL) {
           
           //5.如果該絕對值已經在結點上出現過,則刪除該結點
           if (p[abs(temp->data)] == 1) {
               
               //臨時指針指向temp->next
               r->next = temp->next;
               //刪除temp指向的結點
               free(temp);
               //temp 指向刪除結點下一個結點
               temp = r->next;

           }else
           {
               //6. 未出現過的結點,則將數組中對應位置置爲1;
               p[abs(temp->data)] = 1;
               r = temp;
               //繼續向後遍歷結點
               temp = temp->next;

           }
       }
}

// 調用
        InitList(&L);
        ListInsert(&L, 1, 21);
        ListInsert(&L, 1, -15);
        ListInsert(&L, 1, 15);
        ListInsert(&L, 1, -7);
        ListInsert(&L, 1, 15);

        DeleteEqualNode(&L, 21);
        showList(L);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章