資料結構課程的實踐方法指導
有不少人說資料結構課程抽象,學習起來感到困難。有些同學放棄了,有些同學拿出了高中學習時練熟的功夫,強行理解,死記硬背,結果辛苦不少,效果不佳,反倒得到很多枯燥的感受。
其實,資料結構課中有不少理論性分析的內容,但對於本科生學習的內容,以及要達到目標,還是以實踐性為主的。在學習的方法上做出改變,這門課程就可以展示出生動的實踐性味道來。這就要求在學習過程中,將實踐學習有效地開展下去,諸多的困境即可以破解。
學習資料結構和演算法的過程中做出的這些實踐,也便成了程式設計能力提高中的重要部分。
本文就以資料結構課程開篇的“線性表的順序儲存”部分為例,給出兩種實踐路線的示例。
一、在一個程式檔案中驗證演算法
參考教材中的講解,我們可以將教材中需要掌握的演算法合併成一個程式,讓程式在計算上跑起來。在輸入、除錯、測試的過程中,完成了掌握演算法的目的。
本文示例基於李春葆老師的《資料結構教程(第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
四、執行程式,觀察演算法 法過程
作為學習演算法的過程,建議在需要的時候,通過單步執行的方式,觀察程式的執行過程,以此來理解演算法的執行細節。例如,下圖是上面例子在單步執行過程中的一個截圖。
很顯然,這種將執行過程“視覺化”的方式,對於演算法的學習而言,是非常有效的。