Tải bản đầy đủ (.docx) (67 trang)

Báo cáo project i giải thuật và lập trình c

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (1.58 MB, 67 trang )

Báo cáo Project I 1

Trường đại học Bách Khoa Hà Nội
Viện Công nghệ thông tin & truyền thông
*****

Báo cáo Project I: Giải thuật và lập trình C
Sinh viên thực hiện:
Giảng viên hướng dẫn:

Nguyễn Hồng Quang
Phạm Ngọc Hưng

Hà Nội, tháng 12/2011

Lời cảm ơn
Trước khi vào bài, em xin chân thành cảm ơn các thầy cô trong bộ môn Kỹ thuật máy
tính, viện Công nghệ thông tin và truyền thông đã định hướng em với chủ đề Giải thuật và lập
trình C, đặc biệt, sự góp ý, chỉ bảo của thầy Phạm Ngọc Hưng cùng thầy Nguyễn Hồng Quang
đã giúp đỡ em rất nhiều trong quá trình hoàn thành đề tài này.
Bài viết không tránh khỏi thiếu sót, kính mong thầy cô cùng các bạn góp ý để bài viết
được hoàn thiện hơn, xin chân thành cảm ơn./.
Giải thuật và lập trình C


Báo cáo Project I 2

Giải thuật và lập trình C


Báo cáo Project I 3



Bài 1: Giải thuật sắp xếp
I. Đề bài:
Tìm hiểu các giải thuật sắp xếp dãy (8 giải thuật):
- Sắp xếp kiểu chọn (Selection Sort)
- Sắp xếp nổi bọt (Bubble Sort)
- Sắp xếp kiểu chèn (Insertion Sort)
- Sắp xếp kiểu phân đoạn (Quick sort)
- Sắp xếp kiểu vun đống (Heap Sort)
- Sắp xếp trộn (Merge Sort)
- Sắp xếp bằng phép đếm phân phối (Distribution Counting)
- Sắp xếp bằng cơ số (Radix Sort): Theo kiểu hoán vị các khóa (Radix Exchange Sort) và
sắp xếp cơ số trực tiếp (Straight Radix Sort).
II. Trình bày về các thuật toán:
1. Sắp xếp chèn (Insertion Sort):
Ý tưởng của thuật toán như sau:
+ Bắt đầu bằng một danh sách có thứ tự rỗng
+ Lần lượt chèn thêm các phần tử sắp xếp vào danh sách có thứ tự đó, trong quá trình
chèn phải đảm bảo danh sách vẫn đúng thứ tự.
+ Kết thúc thuật toán ta thu được danh sách đúng thứ tự.
+ Khi sử dụng mảng để lưu trữ thì ta cần dùng 2 mảng A và B, mảng B dùng lưu trữ các
phần tử ban đầu của dãy, mảng A dùng để lưu trữ kết quả của quá trình sắp xếp.
Thời gian tính
Độ phức tạp của thuật toán trong trường hợp cài đặt bằng mảng là O(n2)
Code
void Chen(int a[], int n)
{
int i, j, end;
for(i=1; i{

end=a[i];
j=i-1;
while((j>=0)&&(a[j]>end))
{
a[j+1]=a[j];
j--;
}
a[j+1]=end;
}
}
Thực hiện chương trình:
Giải thuật và lập trình C


Báo cáo Project I 4

2. Sắp xếp lựa chọn (Select Sort):
Ý tưởng của thuật toán như sau:
Tìm phần tử nhỏ nhất đưa vào vị trí thứ 1
Phần tử nhỏ thứ hai đưa vào vị trí thứ 2
Phần tử nhỏ thứ ba đưa vào vị trí thứ 3
Giải thuật và lập trình C


Báo cáo Project I 5
………….
Lặp: (dãy có n phần tử lưu bằng mảng)
Bước 1: - Chọn phần tử bé nhất từ dãy nguồn
- Đổi chỗ cho phần tử A[1]
Bước i:

- Chọn phần tử bé nhất từ dãy nguồn còn lại là a[i],a[i+1],...,a[n]
- Đổi chỗ cho a[i]
Độ phức tạp của thuật toán là O(n2)
Code
void Chon(int a[], int n)
{
int min;
for(int i=0;i{
min=i;
for(int j=i+1;jif(a[j]HoanVi(a[min],a[i]);
}
}
Thực hiện chương trình (với cùng đầu vào như trên):

3. Sắp xếp nổi bọt (Bubble Sort):
Ý tưởng của thuật toán như sau:
Từ dãy ban đầu, thuật toán tiến hành so sánh một phần tử với phần tử ngay sau nó và
thực hiện đổi chỗ nếu chúng không đúng thứ tự.
Giải thuật và lập trình C


Báo cáo Project I 6
Quá trình này lặp lại cho đến khi gặp lần duyệt từ đầu dãy đến cuối dãy mà không phải
thực hiện đổi chỗ.
Độ phức tạp của thuật toán la O(n2)
Code
void NoiBot(int a[], int n)

{
int i, j;
for(i=0; ifor(j=n; j>i; j--)
if(a[j]}
Thực hiện chương trình (với cùng đầu vào như trên)

4. Sắp xếp trộn (Merge Sort)
Ý tưởng của thuật toán như sau:
Thuật toán sắp xếp trộn được phát triển dựa trên phương pháp chia để trị, bao gồm hai
thao tác sau:
+ Chia: Chia dãy ban đầu có n phần tử thành hai dãy, mỗi dãy có n/2 phần tử
+ Trị: Sắp xếp mỗi dãy con một cách đệ quy sử dụng phương pháp sắp xếp trộn, khi
dãy chỉ còn một phần tử thì trả lại phần tử này.
+ Tổ hợp: Trộn hai dãy con đã được sắp xếp để thu được dãy đã sắp xếp gồm tất cả các
phần tử của hai dãy con.
Thời gian tính của trộn
Khởi tạo hai mảng con tạm thời: O(n)
Đưa các phần tử vào mảng kết quả (vòng for cuối cùng): Có n lần lặp, mỗi lần đòi hỏi
thời gian hằng số, do đó thời gian cần thiết để thực hiện la O(n)
Giải thuật và lập trình C


Báo cáo Project I 7
Tổng cộng thời gian là O(n)
Thời gian tính của sắp xếp trộn:
+ Chia: thời gian tính là O(1)
+ Trị: Giải đệ quy hai bài toán con, mỗi bào toán con kích thước n/2,
 2T(n/2)

+ Tổ hợp(trộn): thời gian tính O(n)
Từ các dữ kiện trên ta CM được độ phức tạp của thuật toán là O(n log n);
Code
void Tron(int a[], int start, int end)
{
int mid = (start + end)/2;
int i=start;
int j=mid + 1;
int temp;
while(i<=j&&j<= end)
{
if(a[i]>a[j])
{
temp=a[j];
for(int k =j; k > i; k--)
a[k]=a[k-1];
a[i]=temp;
j++;
}
i++;
}
}
void SXTron(int a[], int start, int end)
{
if(end-start<1)
return;
int mid=(start + end)/2;
SXTron(a, start, mid);
SXTron(a, mid + 1, end);
Tron(a, start, end);

}

Thực hiện chương trình (với cùng đầu vào như trên)

Giải thuật và lập trình C


Báo cáo Project I 8

5. Sắp xếp phân đoạn (sắp xếp nhanh)
Ý tưởng của thuật toán như sau:
Cũng dựa trên phương pháp chia để trị, mô tả đệ quy gần giống như sắp xếp trộn, tuy
nhiên thao tác chia khác so với sắp xếp trộn ở thao tác chọn phần tử chốt.
+ Chọn phần tử trong danh sách làm phần tử chốt.
+ Danh sách sau đó được chia thành hai phần, phần đầu gồm các phần tử nhỏ hơn hoặc
bằng phần tử chốt, phần hai gồm các phần tử lớn hơn phần tử chốt.
+ Sau đó hai danh sách con lại được chọn chốt và chia thành hai danh sách con nhỏ hơn,
đến khi thu được danh sách con chỉ gồm một phần tử.
+ Cuối cùng ta kết hợp hai danh sách con và phần tử chốt cùng mức với nhau lại thì thu
được danh sách đã sắp xếp.
Thời gian tính:
Việc chọn chốt ảnh hưởng lớn đến hiệu quả của thuật toán.
Chốt tồi nhất (là phần tử lớn nhất hoặc nhỏ nhất của dãy) thì thuật toán có độ phức tạp
O(n2), tốt nhất khi ta trọn được chốt mà chia thành hai dãy có số phần tử bằng nhau.
Nếu chọn chốt ngẫu nhiên thì độ phức tạp của thuật toán là O(n log n);
Code
void PhanDoan(int a[], int left, int right)
{
int i, j, x;
x=a[(left+right)/2];

i=left; j=right;
do
{
while(a[i]Giải thuật và lập trình C


Báo cáo Project I 9
while(a[j]>x) j--;
if(i<=j)
{
HoanVi(a[i], a[j]);
i++; j--;
}
}
while(i<=j);
if(leftPhanDoan(a, left, j);
if(jPhanDoan(a, i, right);
}
Thực hiện chương trình (với cùng đầu vào như trên)

6. Sắp xếp vun đống (Heap Sort)
Định nghĩa Heap: Là cây nhị phân thỏa mãn tính chất sau:
+ Tất cả các mức đều đầy, ngoại trừ mức cuối cùng, mức cuối được điền từ trái sang phải
+ Với mỗi nút x phải thỏa mãn tính chất Parent(x)>= x (trường hợp Max Heap)
Ý tưởng sắp xếp dung Heap:
+ Thêm lần lượt các phần tử ban đầu vào Heap rỗng
+ Lần lượt lấy các phàn tử ở gốc sau đó điều chỉnh để thu lại được Heap

Biểu diễn Heap bằng mảng:
+ Nút gốc tương ứng với phần tử số 0 của mảng
+ Nút ở ô có chỉ số k có con trái ở ô có chỉ số 2*k+1, và có con phải ở ô có chỉ số 2*k+2
Thời gian tính:
Thao tác tạo Heap từ n phần tử ban đầu có thời gian là n* O(log n)
Vòng lặp lấy phần tử đầu ra và điều chỉnh lại Heap tốn thời gian
(n-1) O(log n)
Giải thuật và lập trình C


Báo cáo Project I 10
Do đó tổng thời gian thực hiện là O(n log n)
Code
void Shift(int a[], int l, int r)
{
int i, j, x;
i=l;
j=2*i+1;
x=a[i];
while(j<=r)
{
if(jif(a[j]if(a[j]<=x) return;
else
{
a[i]=a[j];
a[j]=x;
i=j;
j=2*i+1;

x=a[i];
}
}
}
void CreatHeap(int a[], int n)
{
int l=n/2-1;
while(l>=0)
{
Shift(a, l, n-1);
l--;
}
}
void VunDong(int a[], int n)
{
int r;
CreatHeap(a, n);
r=n-1;
while(r>0)
{
HoanVi(a[0], a[r]);
r--;
if(r>0)
Shift(a, 0, r);
}
}

Giải thuật và lập trình C



Báo cáo Project I 11

Thực hiện chương trình (với cùng đầu vào như trên)

7. Sắp xếp theo cơ số (Radixsort)
Sắp xếp một mảng số(nguyên hoặc thực) bằng phương pháp sắp xếp cơ số hay còn gọi là
Radixsort.
Khác với các thuật toán trước, Radix sort là một thuật toán tiếp cận theo một hướng hoàn
toàn khác. Nếu như trong các thuật toán khác, cơ sở để sắp xếp luôn là việc so sánh giá trị của 2
phần tử thì Radix sort lại dựa trên nguyên tắc phân loại thư của bưu điện. Vì lý do đó nó còn có
tên là Postman sort. Nó không hề quan tâm đến việc so sánh giá trị của phần tử và bản thân việc
phân loại và trình tự phân loại sẽ tạo ra thứ tự cho các phần tử.
Ý tưởng thuật toán
Coi các phần tử trong mảng sắp xếp được cấu thành từng các lớp có độ ưu tiên khác
nhau. Ví dụ, các số tự nhiên chia thành các lớp như: hàng đơn vị, hàng chục, hàng trăm, hàng
nghìn,..
Bước đầu tiên ta sắp xếp dãy các phần tử bằng cách so sánh các phần tử ở lớp có độ ưu
tiên thấp nhất (ví dụ các chữ số hàng đơn vị). Số nào có hàng đơn vị thấp hơn thì ta đưa lên trên.
Như vậy các số có hàng đơn vị là 0 ở trên cùng, sau đó đến các số có hàng đơn vị là 1,…
Sau bước 1, ta thu được 1 thứ tự sắp xếp mới. Ta lại làm tương tự với các lớp kế tiếp(chữ
số thuộc hàng chục, hàng trăm,…) cuối cùng ta sẽ có dãy đã sắp xếp.
Ví dụ: cho dãy số 766, 432, 7654, 456783, 765, 678, 12, 45678, 64785, 4662
Ta dùng thuật toán sắp xếp theo cơ số 256 nên đầu tiên phải đổi từ hệ thập phân sang cơ số 256,
trong hệ đếm 256 ta có p=b3*2563 + b2*2562 + b1*2561 + b0*2560
Trong đó
b3= a31*27+a30*26+a29*25+a28*24+a27*23+a26+22+a25*21+a24*20
b2= a23*27+a22*26+a21*25+a20*24+a19*23+a18+22+a17*21+a16*20
b1= a15*27+a14*26+a13*25+a12*24+a11*23+a10+22+a9*21+a8*20
Giải thuật và lập trình C



Báo cáo Project I 12

10
9
8
7
6
5
4
3
2
1
Cs

b0= a7*27+a6*26+a5*25+a4*24+a3*23+a2+22+a1*21+a0*20
64785=0+0+17+253
4662=0+0+54+18
766= 0+0+254+2
765=0+0+253+2
432=0+0+176+1
678=0+0166+2
7654=0+0+230+29
45678=0+0+110+178
456783=0+0+223+87
12=0+0+0+12
Phân lô theo b0
766
254
432

176
7654
230
456783 223
765
253
678
166
12
12
45678
110
64785
17
4662
54
12
17
54
110
166
176
A
B0 0
12
17
54
110
166
176


223
223

230
230

253
253

254
254

255

253
253

254

255

Phân lô theo B1
10
9
8
7
6
5
4

3
2
1
Cs

766
765
7654
456783
432
678
45678
4662
64785
12
A

2
2
29
87
1
2
178
18
253
0
B1

0

0

1
1

2
2
2
2

18
18

29
29

87
87

178
178

Phân lô theo B2

Giải thuật và lập trình C


Báo cáo Project I 13
10
9

8
7
6
5
4
3
2
1
Cs

64785
45678
456783
7654
4662
766
765
678
432
12
A

0
0
8
0
0
0
0
0

0
0
B2

0
0
0
0
0
0
0
0
0
0

8
8

255

10
9
8
7
6
5
4
3
2
1

Cs

Phân lô theo B3
456783 0
0
64785
0
0
45678
0
0
7654
0
0
4662
0
0
766
0
0
765
0
0
678
0
0
432
0
0
12

0
0
A
B3 0

255

10
9
8
7
6
5
4
3
2
1
Cs

Lấy các phần tử từ các lô B0,B1,B2,B3 nối lại thành a
456783
64785
45678
7654
4662
766
765
678
432
12

A
0

255
Giải thuật và lập trình C


Báo cáo Project I 14






Giải thích thuật toán:
Ban đầu khởi tạo 4 lô b3,b2,b1,b0
Đổi hệ thập phân sang cơ số 256 và chép vào 4 lô tương ứng
Sắp xếp các số theo lô theo thứ tự b0,b1,b2,b3
Nối các lô b3,b2,b1,b0 lại theo đúng trình tự.
Đánh giá độ phức tạp của thuật toán:
Với một dãy n số, mỗi số dù có tối đa m chữ số, ta luôn biểu diễn được chúng thành 4 chữ số
b3,b2,b1,b0, thuật toán thực hiện 4 lần các thao tác phân lô và ghép lô. Như vậy chi phí cho việc
thực hiện thuật toán là O(n).
Thuật toán không có trường hợp xấu nhất và tồi nhất.
Code
void CoSo(int *a,int n)
{
int *x;
int phan_lo[1000][256];
int dem[256];

int *co_so;
int k,t;
x=(int*)malloc(n*sizeof(int));
co_so=(int*)malloc(n*sizeof(int));
//chep mang can sap sep a[] vao mang tam thoi x[]
for(int i=0;ix[i]=a[i];
//doi co so thap phan sang co so 256
for(k=0;k<25;k=k+8)
{
for(int i=0;ico_so[i]=(x[i]>>k)&255;
//khoi tao bien dem lo
for(int i=0;i<256;i++)
dem[i]=0;
//thuc hien phan lo theo co so 256
for(int j=0;j<256;j++)
{
Giải thuật và lập trình C


Báo cáo Project I 15
int l=0;
for(int i=0;iif(co_so[i]==j)
{
phan_lo[l][j]=x[i];
l++;
dem[j]++;
}

}
//noi cac lo lai voi nhau theo dung trinh tu vao mang tam thoi x[]
t=0;
for(int i=0;i<256;i++)
if(dem[i]>0)
{
int j=0;
while(j{
x[t]=phan_lo[j][i];
t++;
j++;
}
}
}
//chep mang tam thoi da duoc sap sep vao mang a[]
for(int i=0;ia[i]=x[i];
free(x);
free(co_so);
}
Thực hiện chương trình (với cùng đầu vào như trên)

Giải thuật và lập trình C


Báo cáo Project I 16

8. Sắp xếp đếm (Couting sort):
Tư tưởng của thuật toán:

Đầu vào: n số nguyên trong khoảng từ 0-k, trong đó k là số nguyên.
Với mỗi phần tử x của dãy ta xác định hạng (rank) của phần tử đó như là số lượng phần
tử nhỏ hơn x. Khi biết hạng r của x, ta có thể xếp nó vào vị trí r=1. (Ví dụ: có 8 phần tử nhỏ hơn
100 thì ta xếp 100 vào vị trí thứ 9)
Khi có một loạt các phần tử cùng giá trị thì xếp chúng theo thứ tự xuất hiện trong dãy ban
đầu.
Độ phức tạp của thuật toán:
Vòng lặp for đếm dem[i] đòi hỏi thời gian tính Θ(n+k) .
Vòng lặp for tính hạng đòi hỏi thời gian tính Θ (k) .
Vòng lặp for thực hiện sắp xếp đòi hỏi thời gian tính Θ(n+k).
Tổng cộng thời gian tính của thuật toán là Θ(n+k).
Do k=O(n), nên thời gian tính của thuật toán là Θ(n), nghĩa là trong trường hợp (k=O(n))
này nó là một trong những thuật toán tốt nhất. Thật toán này sẽ tồi nếu k>>n.
code
void PhanPhoi(int *a,int n)
{
int max=a[0],t,j=0;
//tim phan tu lon nhat cua mang
for(int i=0; iif(a[i]>max) max=a[i];
//khai bao mang dem [] de dem su lap lai cua tung phan tu trong mang a[]
int dem[max+1];
//khoi tao cho mang dem []
for(int i=0;i<(max+1);i++)
Giải thuật và lập trình C


Báo cáo Project I 17
dem[i]=0;
//dem su lap lai cua cac phan tu trong mang mang a[]

for(int i=0;idem[a[i]]++;
//sap xep mang a[]
for (int i=0;i<(max+1);i++)
{
t=0;
while((dem[i]>0)&&(t{
a[j]=i;
j++;
t++;
}
}
}
Thực hiện chương trình (với cùng đầu vào như trên)

Giải thuật và lập trình C


Báo cáo Project I 18

Bài 2: Cấu trúc dữ liệu danh sách
1. Giới thiệu đối tượng dữ liệu con trỏ
1.1. Cấu trúc dữ liệu tĩnh và cấu trúc dữ liệu động
Với kiểu dữ liệu tĩnh, đối tượng dữ liệu được định nghĩa đệ quy, và tổng kích thước
vùng nhớ dành cho tất cả các biến dữ liệu tĩnh chỉ là 64Kb (1 segment bộ nhớ). Vì lý do
đó, khi có nhu cầu dùng nhiều bộ nhớ hơn ta phải sử dụng các cấu trúc dữ liệu động.
Nhằm đáp ứng nhu cầu thể hiện sát thực bản chất của dữ liệu cũng như xây dựng các
thao tác hiệu quả trên dữ liệu, ta cần phải tìm cách tổ chức kết hợp dữ liệu với những
hình thức linh động hơn, có thể thay đổi kích thước, cấu trúc trong suốt thời gian sống.

Các hình thức tổ chức dữ liệu như vậy được gọi là cấu trúc dữ liệu động. Cấu trúc dữ
liệu động cơ bản nhất là danh sách liên kết.
1.2. Kiểu con trỏ
a) Biến không động
Biến không động (biến tĩnh) là những biến thỏa các tính chất sau:
- Được khai báo tường minh.
- Tồn tại khi vào phạm vi khai báo và chỉ mất khi ra khỏi phạm vi này.
- Được cấp phát vùng nhớ trong vùng dữ liệu (Data segment) hoặc là Stack (đối với
các biến nửa tĩnh, các biến cục bộ).
- Kích thước không thay đổi trong suốt quá trình sống.
b) Kiểu dữ liệu con trỏ
Khi nói đến kiểu dữ liệu T, ta thường chú ý đến hai đặc trưng quan trọng và liên hệ mật
thiết với nhau:
- Tập V các giá trị thuộc kiểu: đó là tập các giá trị hợp lệ mà đối tượng kiểu T có thể
nhận được và lưu trữ.
- Tập O các phép toán (hay thao tác xử lý) xác định có thể thực hiện trên các đối
tượng dữ liệu kiểu đó.
Kí hiệu: T = <V, O>
c) Định nghĩa kiểu dữ liệu con trỏ
Cho trước kiểu T=<V,O>
Kiểu con trỏ - kí hiệu “Tp” – chỉ đến các phần tử có kiểu “T” được định nghĩa:
Tp=<Vp, Op>, trong đó:
- Vp={{các địa chỉ có thể lưu trữ những đối tượng có kiểu T}, NULL} (với NULL là
một giá trị đặc biệt tượng trưng cho một giá trị không biết hoặc không quan tâm).
- Op = {các thao tác định địa chỉ của một đối tượng thuộc kiểu T khi biết con trỏ chỉ
đến đối tượng đó} (thường gồm các thao tác tạo một con trỏ chỉ đến một đối tượng
thuộc kiểu T; hủy một đối tượng thuộc kiểu T khi biết con trỏ chỉ đến đối tượng đó)
Kiểu con trỏ là kiểu cơ sở dùng lưu địa chỉ của một đối tượng dữ liệu khác.
Biến thuộc kiểu con trỏ Tp là biến mà giá trị của nó là địa chỉ của một vùng nhớ ứng
với một biến kiểu T, hoặc là giá trị NULL.

Kích thước của biến con trỏ tùy thuộc vào quy ước số byte địa chỉ trong từng mô hình
bộ nhớ của từng ngôn ngữ lập trình cụ thể. Chẳng hạn biến con trỏ trong C++ trên môi
trường Windows có kích thước 4 bytes.
Cú pháp định nghĩa kiểu con trỏ trong ngôn ngữ C, C++:
typedef <kiểu cơ sở> *<kiểu con trỏ>;
Ví dụ:
typedef int *intpointer; //Kiểu con trỏ
Giải thuật và lập trình C


Báo cáo Project I 19
intpointer p; //Biến con trỏ
Cú pháp định nghĩa trực tiếp một biến con trỏ trong ngôn ngữ C, C++:
<kiểu cơ sở> *<tên biến>;
Ví dụ:
int *p;
Các thao tác cơ bản trên kiểu con trỏ (minh họa bằng C++)
Khi một biến con trỏ p lưu địa chỉ của đối tượng x, ta nói “p trỏ đến x”.
- Gán địa chỉ của một vùng nhớ con trỏ p:
p = <địa chỉ>;
p = <địa chỉ>+<giá trị nguyên>;
- Truy xuất nội dung của đối tượng do p trỏ đến: *p
d) Biến động
Trong nhiều trường hợp, tại thời điểm biên dịch không thể xác định trước kích thước
chính xác của một số đối tượng dữ liệu do sự tồn tại và tăng trưởng của chúng phụ
thuộc vào ngữ cảnh của việc thực hiện chương trình.
Các đối tượng có đặc điểm như vậy được khai báo như biến động.
Đặc trưng của biến động
- Biến không được khai báo tường minh.
- Có thể được cấp phát hoặc giải phóng bộ nhớ khi người sử dụng yêu cầu.

- Các biến này không theo qui tắc phạm vi (tĩnh).
- Vùng nhớ của biến được cấp phát trong Heap.
- Kích thước có thể thay đổi trong quá trình sống.
Do không được khai báo tường minh nên các biến động không có một định danh được
kết buộc với địa chỉ vùng nhớ cấp phát cho nó, do đó gặp khó khăn khi truy xuất đến
một biến động.
Để giải quyết vấn đề này, phải dùng một con trỏ (là biến không động) để trỏ đến biến
động. Khi tạo ra một biến động, phải dùng một con trỏ để lưu địa chỉ của biến này
thông qua biến con trỏ đã biết định danh.
Hai thao tác cơ bản trên biến động là tạo và hủy một biến động do biến con trỏ “p” trỏ
đến:
- Tạo ra một biến động và cho con trỏ “p” chỉ đến nó (minh họa bằng C++: dùng hàm
new để cấp phát bộ nhớ):
p = new KieuCoSo;
- Hủy vùng nhớ cấp phát bởi hàm new do p trỏ tới (dùng hàm delete)
delete(p);
Cấp phát bộ nhớ cho mảng động
KieuCoSo *p;
p = new KieuCoSo[Max];/Max là già trị nguyên dương
Thu hồi vùng nhớ của mảng động
delete([]p);
3.2. Danh sách liên kết
3.2.1. Định nghĩa
Cho T là một kiểu được định nghĩa trước, kiểu danh sách Tx gồm các phần tử thuộc
kiểu T được định nghĩa là:
Tx = <Vx, Ox>
Trong đó:
Giải thuật và lập trình C



Báo cáo Project I 20
- Vx = {tập hợp có thứ tự các phần tử kiểu T được móc nối với nhau theo trình tự
tuyến tính};
- Ox = {các thao tác trên danh sách liên kết như: tạo danh sách; tìm một phần tử trong
danh sách; chèn một phần tử vào danh sách; hủy một phần tử khỏi danh sách; sắp
xếp danh sách…}
Ví dụ:
Hồ sơ các học sinh cụa một trường được tổ chức thành danh sách gồm nhiều hồ sơ của
từng học sinh, số lượng học sinh trong trường có thể thay đổi do vậy cần có các thao tác
thêm, hủy một hồ sơ. Để phục vụ cho công tác giáo vụ cần thực hiện các thao tác tìm hồ
sơ của một học sinh, in danh sách hồ sơ…
3.2.2. Tổ chức danh sách liên kết
Mối liên hệ giữa các phần tử được thể hiện ngầm
- Mỗi phần tử trong danh sách được đặc trưng bởi một chỉ số, cặp phần tử xi, xi+1
được xác định là kế cận trong danh sách nhờ vào quan hệ của cặp chỉ số I và i+1.
Các phần tử trong danh sách buộc phải lưu trữ liên tiếp trong bộ nhớ để có thể xây
dựng công thức xác định địa chỉ của phần tử thứ i: address(i) = address(1) + (i1)*sizeof(T).
- Có thể xem mảng và tập tin là những danh sách đặc biệt được tổ chức theo hình thức
liên kết “ngầm” giữa các phần tử.
- Với cách tổ chức này có ưu điểm là cho phép truy xuất ngẫu nhiên, đơn giản và
nhanh chóng đến một phần tử bất kỳ trong danh sách. Tuy nhiên hạn chế của nó là
hạn chề về mặt sử dụng bộ nhớ.
- Đối với mảng, số phần tử được xác định trong thời gian biên địch và cần cấp phát bộ
nhớ liên tục. Trong trường hợp kích thước bộ nhớ trống còn đủ để chứa toàn bộ
mảng nhưng các ô nhớ trống lại không nằm kế cận nhau thì cũng không cấp phát
vùng nhớ cho mảng được. Ngoài ra do kích thước mảng cố định mà số phần tử của
danh sách lại khó dự trù chính xác nên có thể gây ra tình trạng thiếu hụt hay lãng phí
bộ nhớ. Các thao tác thêm hủy các phân tử thục hiện không tự nhiên.
Mối liên hệ giữa các phần tử được thể hiện tường minh
- Mỗi phần tử ngoài thông tin về bản thân còn chứa một liên kết (địa chỉ) đến phần tử

kế trong danh sách nên còn được gọi là danh sách móc nối (liên kết).
- Do liên kết tường minh nên các phần tử trong danh sách không cần phải lưu trữ kế
cận trong bộ nhớ. Nhờ đó khắc phục được các khuyết điểm của hình thức tổ chức
mảng. Tuy nhiên việc truy xuất đòi hỏi phải thực hiện truy xuất thông qua một số
phần tử khác.
- Có nhiều kiểu tổ chức liên kết giữa các phần tử trong danh sách như: danh sách liên
kết đơn, danh sách liên kết kép, vòng,…
3.3. Danh sách liên kết đơn
Danh sách liên kết đơn tổ chức theo cách cấp phát liên kết. Các thao tác cơ bản trên
danh sách đơn gồm có tạo phần tử, chèn phần tử vào danh sách, hủy một phần tử trong
danh sách, tìm kiếm phần tử trong danh sách và sắp xếp danh sách.
3.3.1. Tổ chức danh sách theo cách cấp phát liên kết.
Một danh sách liên kết bao gồm tập các phần tử (nút), mỗi nút là một cấu trúc chứa hai
thông tin:
- Thành phần dữ liệu: lưu trữ các thông tin về bản thân phần tử.
- Thành phần mối liên kết: lưu trữ địa chỉ của phần tử kế tiếp trong danh sách, hoặc
Giải thuật và lập trình C


Báo cáo Project I 21
lưu trữ giá trị NULL nếu là phần tử cuối danh sách.
Mỗi nút như trên có thể được cài đặt như sau:
struct tagNode
{
Data Info; //thành phần dữ liệu
tagNode* pNext; //thành phần mối liên kết (tự trỏ)
};
//Đổi lại tên kiểu phần tử trong danh sách
typedef tagNode NODE;
Mỗi phần tử trong danh sách đơn là một biến động, sẽ được yêu cầu cấp phát bộ nhớ

khi cần.
Danh sách liên kết đơn chính là sự liên kết các biến động này với nhau, do vậy đạt được
sự linh động khi thay đổi số lượng các phần tử.
Ví dụ:
Để quản lý một danh sách liên kết đơn chỉ cần biết địa chỉ phần tử đầu danh sách, từ
phần tử đầu danh sách ta có thể đi đến các nút tiếp theo trong danh sách liên kết nhờ
vào thành phần địa chỉ của nút.
Con trỏ pHead được dùng để lưu trữ địa chỉ của phần tử ở đầu danh sách, ta gọi Head là
đầu của danh sách. Ta có khai báo:
NODE* pHead;
Để tiện lợi, ta có thể sử dụng thêm một con trỏ pTail để giữ địa chỉ phần tử cuối danh
sách. Khai báo pTail như sau:
NODE* pTail;
3.3.2. Định nghĩa cấu trúc danh sách liên kết
Giả sử ta có định nghĩa nút trong danh sách như sau:
struct tagNode
{
Data Info;
tagNode* pNext;
};
typedef tagNode NODE;
Định nghĩa kiểu danh sách liên kết: LIST
struct LIST
{
NODE* pHead; //con trỏ lưu trữ địa chỉ đầu DSLK
NODE* pTail; //con trỏ lưu trữ địa chỉ cuối DSLK
};
Ví dụ: định nghĩa danh sách đơn lưu trữ hồ sơ sinh viên:
//Kiểu thành phần dữ liệu Data: SV
struct SV //Data

{
char Ten[30];
int MaSV;
};
//Kiểu phần tử trong DS: SinhvienNode
struct SinhvienNode
Giải thuật và lập trình C


Báo cáo Project I 22
{
SV Info;
SinhvienNode* pNext;
};
//Đổi lại tên kiểu phần tử trong DS: SVNode
typedef SinhvienNode SVNode;
//Định nghĩa danh sách liên kết: LIST
struct LIST
{
SVNode * pHead;
SVNode * pTail;
};
3.3.3. Các thao tác cơ bản trên danh sách liên kết đơn
- Tạo một phần tử cho danh sách liên kết với thông tin x.
- Khởi tạo danh sách rỗng.
- Kiểm tra danh sách rỗng.
- Duyệt danh sách.
- Tìm một phần tử trong danh sách.
- Chèn một phần tử vào danh sách
- Hủy một phần tử khỏi danh sách

- Sắp xếp danh sách
Giả sử với định nghĩa danh sách liên kết như trên, ta có cài đặt các thao tác như sau:
1. Tạo phần tử
Hàm CreateNode(x): tạo một nút trong DSLK vời thành phần dữ liệu x, hàm trả về con
trỏ lưu trữ địa chỉ của phần tử vừa tạo (nếu thành công).
NODE* GetNode(Data x)
{
NODE *p;
// cấp phát vùng nhớ cho phần tử
p = new NODE;
if ( p==NULL)
{
cout<<“Loi cap phat";
return NULL; //exit(1);
}
p ->Info = x; // gán thông tin cho phần tử p
p->pNext = NULL;
return p;
}
Sử dụng hàm CreateNode: gán giá trị của hàm (là con trỏ) cho 1 biến con trỏ kiểu
NODE, phần tử này đặt tên là new-ele, giữ địa chỉ của phần tử đã tạo:
NODE *new_ele = CreateNode(x);
2. Khởi tạo danh sách rỗng l
void CreatList(LIST &l)
{
l.pHead = l.pTail = NULL;
Giải thuật và lập trình C


Báo cáo Project I 23

}
3. Kiểm tra danh sách rỗng
Hàm IsEmpty(l) = 1 nếu danh sách rỗng, bằng 0 nếu danh sách l không rỗng
int IsEmpty(LIST l)
{
if (l.pHead == NULL) // DS rỗng
return 1;
return 0;
}
4. Duyệt danh sách
Duyệt danh sách là thao tác thường được thực hiện khi có nhu cầu xử lý các phần tử của
danh sách theo cùng một cách thức hoặc khi cần lấy thông tin tổng hợp từ các phần tử
của danh sách như:
• Đếm các phần tử của danh sách.
• Xuất dữ liệu các phần tử trong DS ra màn hình
• Tìm tất cả các phần tử thỏa điều kiện
• Hủy toàn bộ danh sách (và giải phóng bộ nhớ)
Thuật toán có thể mô tả như sau:
Bước 1: p = Head; //Cho p trỏ đến phần tử đầu danh sách
Bước 2: Trong khi (Danh sách chưa hết) thực hiện
B2.1 : Xử lý phần tử p;
B2.2 : p:=p->pNext; // Cho p trỏ tới phần từ kế tiếp
Cài đặt:
void ProcessList (LIST l)
{
NODE *p;
p = l.pHead;
while (p!= NULL)
{
ProcessNode(p); // xử lý cụ thể từng ứng dụng

p = p->pNext;
}
}
Việc xuất dữ liệu ra màn hình cũng chính là hàm duyệt danh sách, trong đó với mỗi nút
hàm duyệt qua, ta xuất thông tin của nút đó ra màn hình (tức là thủ tục ProcessNode(p)
chính là thủ tục xuất thông tin nút p ra màn hình).
void XuatDS(LIST l)
{
NODE *p;
if(IsEmpty(l))
{
cout<<"\nDS rong!\n"; return;
}
cout<<"\nDu lieu cua Danh sach:\n";
p = l.pHead;
while (p!= NULL)
Giải thuật và lập trình C


Báo cáo Project I 24
{
cout<<(p->Info)<<'\t'; // ProcessNode(p)
p = p->pNext;
}
}
5. Tìm phần tử có thành phần dữ liệu x trong danh sách
Mô tả
Bước 1: p = Head; //Cho p trỏ đến phần tử đầu danh sách
Bước 2: Trong khi (p != NULL) và (p->Info != x ) thực hiện:
p = p->Next;// Cho p trỏ tới phần tử kế tiếp

Bước 3:
Nếu p != NULL thì p trỏ tới phần tử cần tìm
Ngược lại: không có phần tử cần tìm.
Cài đặt:
/*
Search(l,x) = p; //p là con trỏ chỉ đến phần tử có
chứa thông tin x cần tìm
= NULL; //ngược lại
*/
NODE *Search(LIST l, Data x)
{
NODE *p;
p = l.pHead;
while((p!= NULL)&&(p->Info != x))
p = p->pNext;
return p;
}
6. Thao tác chèn một phần tử vào danh sách
Có ba vị trí để có thể chèn một phần tử new_ele vào danh sách:
� Chèn phần tử vào đầu danh sách
� Chèn phần tử vào cuối danh sách
� Chèn phần tử vào danh sách sau một phần tử q.
a. Chèn phần tử vào đầu danh sách
Mô tả
Kiểm tra nếu danh sách rỗng //chèn nút vào danh sách rỗng
Head = new_ele;
Tail = Head;
Ngược lại
New_ele ->pNext = Head;
Head = new_ele;

Minh họa
Cài đặt
/*AddFirst(l,new_ele): chèn một phần tử new_ele vào đầu danh sách*/
void AddFirst(LIST &l, NODE* new_ele)
{
if (l.pHead==NULL) //danh sách rỗng:
Giải thuật và lập trình C


Báo cáo Project I 25
if(IsEmpty(l))
{
l.pHead = new_ele;
l.pTail = l.pHead;
}
else
{
Newele->pNext = l.pHead;
l.pHead = new_ele;
}
}
Tạo nút có dữ liệu x, sau đó chèn nút vừa tạo vào đầu danh sách, hàm trả về con trỏ
lưu vị trí nút vừa tạo.
NODE* InsertHead(LIST &l, Data x)
{
NODE* new_ele = CreateNode(x);
if (new_ele ==NULL) return NULL;
if (l.pHead==NULL)
{
l.pHead = new_ele; l.pTail = l.pHead;

}
else
{
new_ele->pNext = l.pHead;
l.pHead = new_ele;
}
return new_ele;
}
Hoặc
NODE* InsertHead(LIST &l, Data x)
{
NODE* new_ele = CreateNode(x);
if (new_ele ==NULL) return NULL;
AddHead(l, new_ele);
return new_ele;
}
b. Chèn một phần tử vào cuối danh sách
Mô tả:
Nếu danh sách rỗng thì
Head = new_ele;
Tail = Head;
Ngược lại
Tail ->Next = new_ele;
Tail = new_ele ;
Minh họa
Cài đặt
Giải thuật và lập trình C



×