1. 程式人生 > >資料結構課程的實踐方法指導

資料結構課程的實踐方法指導

  有不少人說資料結構課程抽象,學習起來感到困難。有些同學放棄了,有些同學拿出了高中學習時練熟的功夫,強行理解,死記硬背,結果辛苦不少,效果不佳,反倒得到很多枯燥的感受。
  其實,資料結構課中有不少理論性分析的內容,但對於本科生學習的內容,以及要達到目標,還是以實踐性為主的。在學習的方法上做出改變,這門課程就可以展示出生動的實踐性味道來。這就要求在學習過程中,將實踐學習有效地開展下去,諸多的困境即可以破解。
  學習資料結構和演算法的過程中做出的這些實踐,也便成了程式設計能力提高中的重要部分。
  本文就以資料結構課程開篇的“線性表的順序儲存”部分為例,給出兩種實踐路線的示例。

一、在一個程式檔案中驗證演算法

  參考教材中的講解,我們可以將教材中需要掌握的演算法合併成一個程式,讓程式在計算上跑起來。在輸入、除錯、測試的過程中,完成了掌握演算法的目的。
  本文示例基於李春葆老師的《資料結構教程(第4版)》給出,使用其他教材的做法類似:
  第一步:定義資料的儲存結構
  例如,對於線性結構,可以寫下:

#define MaxSize 50    //Maxsize將用於後面定義儲存空間的大小
typedef int ElemType;  //ElemType在不同場合可以根據問題的需要確定,在此取簡單的int
typedef struct
{
    ElemType data[MaxSize];  //利用了前面MaxSize和ElemType的定義
int length; } SqList;

  第二步:實現各種基本操作
  例如,對於線性表的順序儲存

//用陣列建立線性表
void CreateList(SqList *&L, ElemType a[], int n)
{
    int i;
    L=(SqList *)malloc(sizeof(SqList));
    for (i=0; i<n; i++)
        L->data[i]=a[i];
    L->length=n;
}

//初始化線性表InitList(L)
void InitList(SqList *&L)   //引用型指標
{ L=(SqList *)malloc(sizeof(SqList)); //分配存放線性表的空間 L->length=0; } //銷燬線性表DestroyList(L) void DestroyList(SqList *&L) { free(L); } //判定是否為空表ListEmpty(L) bool ListEmpty(SqList *L) { return(L->length==0); } //求線性表的長度ListLength(L) int ListLength(SqList *L) { return(L->length); } //輸出線性表DispList(L) void DispList(SqList *L) { int i; if (ListEmpty(L)) return; for (i=0; i<L->length; i++) printf("%d ",L->data[i]); printf("\n"); } //求某個資料元素值GetElem(L,i,e) bool GetElem(SqList *L,int i,ElemType &e) { if (i<1 || i>L->length) return false; e=L->data[i-1]; return true; } //按元素值查詢LocateElem(L,e) int LocateElem(SqList *L, ElemType e) { int i=0; while (i<L->length && L->data[i]!=e) i++; if (i>=L->length) return 0; else return i+1; } //插入資料元素ListInsert(L,i,e) bool ListInsert(SqList *&L,int i,ElemType e) { int j; if (i<1 || i>L->length+1) return false; //引數錯誤時返回false i--; //將順序表邏輯序號轉化為物理序號 for (j=L->length; j>i; j--) //將data[i..n]元素後移一個位置 L->data[j]=L->data[j-1]; L->data[i]=e; //插入元素e L->length++; //順序表長度增1 return true; //成功插入返回true } //刪除資料元素ListDelete(L,i,e) bool ListDelete(SqList *&L,int i,ElemType &e) { int j; if (i<1 || i>L->length) //引數錯誤時返回false return false; i--; //將順序表邏輯序號轉化為物理序號 e=L->data[i]; for (j=i; j<L->length-1; j++) //將data[i..n-1]元素前移 L->data[j]=L->data[j+1]; L->length--; //順序表長度減1 return true; //成功刪除返回true }

   第三步,編寫測試函式,對於每一個基本運算的實現進行測試
  首先,應該在程式頭部加上必要的標頭檔案(如果不加,出現編譯錯誤倒是也會對付)。

#include <stdio.h>
#include <malloc.h>

  測試函式一般“宜小不宜大”,也就是說,一次儘可能測試儘可能少的基本操作。不過,在必要的時候,也需要呼叫別的基本操作,用來完成輔助性的工作。例如,下面的main函式,目標是測試建立順序表的函式CreateList,建立中需要的陣列x可以在程式中直接初始化,而建立的結果,希望通過輸出進行觀察,於是,呼叫DispList成為必要。
  用於測試CreateList的測試函式寫作:

int main()
{
    SqList *sq;
    ElemType x[6]= {5,8,7,2,4,9};
    CreateList(sq, x, 6);
    DispList(sq);
    return 0;
}

   再例,測試初始化操作InitList時,可以與插入元素的操作ListInsert一起進行。

int main()
{
    SqList *sq;
    InitList(sq);
    ListInsert(sq, 1, 5);
    ListInsert(sq, 2, 3);
    ListInsert(sq, 1, 4);
    DispList(sq);
    return 0;
}

  接下來,可以更換測試函式,用於測試別的基本操作。“宜小不宜大”的原則簡化了工作的難度,但是可能需要編制多個測試函式,分多次執行,才能將多個測試函式測試完。

二、構建自己的“演算法庫”

  資料結構課程的主線就是各種邏輯結構,在不同的儲存結構下,基本運算的實現與複雜度分析。這些基本資料結構的定義,以及各種基本運算的實現,可以構成“演算法庫”。建立起來的演算法庫,可以作為即將要解決問題時,所依託的“基礎設施”。事實上,在工程中用到的系統庫,或第三方的演算法庫,也是將通用的模組集中起來而形成的。
  為有效地組織,程式將用多檔案的方式組織,其中的標頭檔案和提供自定函式實現的檔案,組成的就是演算法庫。
  對於線性表的順序儲存,演算法庫的標頭檔案可以定義為:
list.h

#ifndef LIST_H_INCLUDED
#define LIST_H_INCLUDED

#define MaxSize 50
typedef int ElemType;
typedef struct
{
    ElemType data[MaxSize];
    int length;
} SqList;
void CreateList(SqList *&L, ElemType a[], int n);//用陣列建立線性表
void InitList(SqList *&L);//初始化線性表InitList(L)
void DestroyList(SqList *&L);//銷燬線性表DestroyList(L)
bool ListEmpty(SqList *L);//判定是否為空表ListEmpty(L)
int ListLength(SqList *L);//求線性表的長度ListLength(L)
void DispList(SqList *L);//輸出線性表DispList(L)
bool GetElem(SqList *L,int i,ElemType &e);//求某個資料元素值GetElem(L,i,e)
int LocateElem(SqList *L, ElemType e);//按元素值查詢LocateElem(L,e)
bool ListInsert(SqList *&L,int i,ElemType e);//插入資料元素ListInsert(L,i,e)
bool ListDelete(SqList *&L,int i,ElemType &e);//刪除資料元素ListDelete(L,i,e)#endif // LIST_H_INCLUDED
#endif

  配套地,在list.cpp中實現這些基本操作。
  
list.cpp

#include <stdio.h>
#include <malloc.h>
#include "list.h"

//用陣列建立線性表
void CreateList(SqList *&L, ElemType a[], int n)
{
    int i;
    L=(SqList *)malloc(sizeof(SqList));
    for (i=0; i<n; i++)
        L->data[i]=a[i];
    L->length=n;
}

//初始化線性表InitList(L)
void InitList(SqList *&L)   //引用型指標
{
    L=(SqList *)malloc(sizeof(SqList));
    //分配存放線性表的空間
    L->length=0;
}

//銷燬線性表DestroyList(L)
void DestroyList(SqList *&L)
{
    free(L);
}

//判定是否為空表ListEmpty(L)
bool ListEmpty(SqList *L)
{
    return(L->length==0);
}

//求線性表的長度ListLength(L)
int ListLength(SqList *L)
{
    return(L->length);
}

//輸出線性表DispList(L)
void DispList(SqList *L)
{
    int i;
    if (ListEmpty(L)) return;
    for (i=0; i<L->length; i++)
        printf("%d ",L->data[i]);
    printf("\n");
}

//求某個資料元素值GetElem(L,i,e)
bool GetElem(SqList *L,int i,ElemType &e)
{
    if (i<1 || i>L->length)  return false;
    e=L->data[i-1];
    return true;
}

//按元素值查詢LocateElem(L,e)
int LocateElem(SqList *L, ElemType e)
{
    int i=0;
    while (i<L->length && L->data[i]!=e) i++;
    if (i>=L->length)  return 0;
    else  return i+1;
}

//插入資料元素ListInsert(L,i,e)
bool ListInsert(SqList *&L,int i,ElemType e)
{
    int j;
    if (i<1 || i>L->length+1)
        return false;   //引數錯誤時返回false
    i--;            //將順序表邏輯序號轉化為物理序號
    for (j=L->length; j>i; j--) //將data[i..n]元素後移一個位置
        L->data[j]=L->data[j-1];
    L->data[i]=e;           //插入元素e
    L->length++;            //順序表長度增1
    return true;            //成功插入返回true
}

//刪除資料元素ListDelete(L,i,e)
bool ListDelete(SqList *&L,int i,ElemType &e)
{
    int j;
    if (i<1 || i>L->length)  //引數錯誤時返回false
        return false;
    i--;        //將順序表邏輯序號轉化為物理序號
    e=L->data[i];
    for (j=i; j<L->length-1; j++) //將data[i..n-1]元素前移
        L->data[j]=L->data[j+1];
    L->length--;              //順序表長度減1
    return true;              //成功刪除返回true
}

  大功告成。如果這些函式的實現還沒有經過測試的話,編制main.cpp,在其中寫測試函式完成測試工作。
  例如,為測試CreateList,寫
  
main.cpp

#include "list.h"
int main()
{
    SqList *sq;
    ElemType x[6]= {5,8,7,2,4,9};
    CreateList(sq, x, 6);
    DispList(sq);
    return 0;
}

三、應用資料結構求解問題

  要實現的內容必定是要基於特定的儲存結構,可能利用各基本操作的組合就可以完成,也可能需要在資料結構上,自己設計演算法完成。也不排除主要部分是專門設計的演算法,但有些環節,也需要基本操作的支援。總之,可以看出,從解決問題的角度,需要綜合運用知識了,同時,前面建的演算法庫,有了用武之地。
  例如:設順序表有10個元素,其元素型別為整型。設計一個演算法,以第一個元素為分界線,將所有小於它的元素移到該元素的前面,將所有大於它的元素移到該元素的後面。
  設計出的演算法是:

void move1(SqList *&L)
{
    int i=0,j=L->length-1;
    ElemType pivot=L->data[0];
    ElemType tmp;
    while (i<j) 
    {
        while (i<j && L->data[j]>pivot)
            j--;    
        while (i<j && L->data[i]<=pivot)
            i++;    
        if (i<j)
        {
            tmp=L->data[i];
            L->data[i]=L->data[j];
            L->data[j]=tmp;
        }
    }
    tmp=L->data[0]; 
    L->data[0]=L->data[j];
    L->data[j]=tmp;
}

  為了將其作為一個程式,通過執行觀察程式的執行過程,可以寫出如下的main.cpp。可以看出,前面編制好的標頭檔案list.h和list.cpp正在支援著這些工作。
  
main.cpp

#include "list.h"
void move1(SqList *&L)  //定義解決問題的演算法
{
    int i=0,j=L->length-1;
    ElemType pivot=L->data[0];
    ElemType tmp;
    while (i<j)
    {
        while (i<j && L->data[j]>pivot)
            j--;
        while (i<j && L->data[i]<=pivot)
            i++;
        if (i<j)
        {
            tmp=L->data[i];
            L->data[i]=L->data[j];
            L->data[j]=tmp;
        }
    }
    tmp=L->data[0];
    L->data[0]=L->data[j];
    L->data[j]=tmp;
}
int main()   //在main函式中呼叫,保證程式能執行,解決問題
{
    SqList *sq;
    ElemType x[10]= {3, 8, 2, 7, 1, 5, 3, 4, 6, 0};
    CreateList(sq, x, 10);
    DispList(sq);
    move1(sq);
    DispList(sq);
    return 0;
}

程式執行結果:

3 8 2 7 1 5 3 4 6 0
1 0 2 3 3 5 7 4 6 8

四、執行程式,觀察演算法 法過程

  作為學習演算法的過程,建議在需要的時候,通過單步執行的方式,觀察程式的執行過程,以此來理解演算法的執行細節。例如,下圖是上面例子在單步執行過程中的一個截圖。
這裡寫圖片描述
  很顯然,這種將執行過程“視覺化”的方式,對於演算法的學習而言,是非常有效的。