You are on page 1of 35

1

CHƯƠNG 3

DANH SÁCH

3.1. KHÁI NIỆM VỀ DANH SÁCH

3.1.1. Các khái niệm

Danh sách là một dãy hữu hạn các phần tử cùng loại được sắp xếp theo một thứ
tự tuyến tính.

Danh sách L gồm các phần tử a1, a2,..., an được kí hiệu: L = (a1, a2,..., an).

Trong đó:

- n được gọi là chiều dài của danh sách

- ai là phần tử thứ i của danh sách

- a1 là phần tử đầu tiên của danh sách

- an là phần tử cuối cùng của danh sách

- Nếu n = 0 thì danh sách được gọi là rỗng.

Một tính chất quan trọng của danh sách là các phần tử được sắp xếp tuyến tính
theo vị trí của chúng trong danh sách. Với n>1, i = 1, 2,..., n-1, phần tử ai là phần tử
ngay trước phần tử ai+1 và ai+1 là phần tử ngay sau ai.

Trong một danh sách các phần tử có thể giống nhau.

Danh sách con

Cho L = (a1, a2,..., an) là một danh sách và i, j là các vị trí trong danh sách
(1ijn). Danh sách L’ = (b1, b2,..., bj-1+1) trong đó b1 = ai, b2 = ai+1,..., bj-i+1 được
gọi là danh sách con của danh sách L.

Dãy con

Danh sách L’ được tạo thành từ danh sách L bằng cách bỏ đi một số phần tử
của danh sách L nhưng vẫn giữ nguyên thứ tự các phần tử được gọi là dãy con của
danh sách L.
2
Ví dụ:

L = (3, 1, 7, 5, 3, 4, 7, 5).

L1 = (7, 5, 3, 4) là danh sách con của L.

L2 = (1, 5, 4, 5) là dãy con của L.

Trong thực tế có rất nhiều dữ liệu được tổ chức dưới dạng danh sách, như: danh
sách sinh viên, danh sách môn học, danh bạ điện thoại,...

Tùy thuộc từng loại danh sách sẽ có các thao tác đặc trưng riêng, bên cạnh đó,
trên các danh sách thường thực hiện các thao tác cơ bản sau:

- Khởi tạo danh sách: tạo một danh sách rỗng

- Thêm một phần tử vào danh sách tại vị trí p

- Loại bỏ một phần tử tại vị trí p khỏi danh sách

- Tìm kiếm một phần tử trong danh sách

- Sắp xếp danh sách

- Liệt kê các phần tử trong danh sách

- Ghép nhiều danh sách thành một danh sách

- Tách một danh sách thành nhiều danh sách

- Sao chép một danh sách

- ...

3.1.2. Biểu diễn danh sách tuyến tính trên máy tính

Việc cài đặt một danh sách trong máy tính là tìm một cấu trúc dữ liệu cụ thể
mà máy tính hiểu được để lưu trữ các phần tử của danh sách, đồng thời viết các đoạn
chương trình con mô tả các thao tác cần thiết đối với danh sách đó.

Có 2 phương pháp biểu diễn danh sách tuyến tính trên máy tính, đó là:

- Mảng

- Danh sách liên kết


3
3.2. BIỂU DIỄN DANH SÁCH BẰNG MẢNG

3.2.1. Tổ chức dữ liệu

Mảng là một cấu trúc dữ liệu cơ bản, thường dùng và được các ngôn ngữ lập
trình cấp cao hỗ trợ. Mảng là một dãy cố định các ô nhớ chứa các phần tử cùng kiểu.
Mỗi ô nhớ của mảng được xác định bởi chỉ số.

Với cách cài đặt này, ta phải ước lượng số phần tử của danh sách để khai báo
số phần tử của mảng cho thích hợp. Dễ thấy rằng số phần tử của mảng phải được
khai báo không ít hơn số phần tử của danh sách.

Cho danh sách L = (a1, a2,..., an) với mỗi phần tử ai có kiểu ElementType. Tổ
chức dữ liệu kiểu mảng để lưu danh sách L gồm 2 thành phần:

- Thành phần element: là mảng lưu các phần tử của danh sách.

- Thành phần count: là vị trí của ô nhớ lưu phần tử cuối cùng của danh sách,
cũng là số phần tử hiện có của danh sách.

1 a1
 Các ô nhớ trống   Các phần tử của danh sách

2 a2
... ...
i ai
... ...
count an
.
.
.
MaxLength .
Chỉ số Mảng

Hình 4.1. Mảng một chiều

Khai báo một danh sách như sau:


const int MaxLength = ...; //Số phần tử tối đa của danh sách

typedef elementType ...; //Định nghĩa kiểu phần tử của danh sách
4
struct ListArr {

elementType element[MaxLength];

int count;

};

ListArr L;

Ví dụ: Khai báo danh sách sinh viên gồm các thông tin: mã SV, họ tên, lớp.
const int MaxLength = 50;

typedef struct student{ //Định nghĩa kiểu phần tử của danh sách

char [10] id;

char [50] name;

char [8] class;

};

struct ListST{ //Định nghĩa danh sách sinh viên

student element[MaxLength];

int count;

};

ListST st; //Khai báo một danh sách sinh viên

3.2.2. Cài đặt các thao tác trên danh sách

Khởi tạo danh sách rỗng: InitList (L)

Danh sách rỗng có độ dài bằng 0. Biến count chỉ vị trí cuối cùng, cũng là độ
dài của danh sách, vì vậy, để khởi tạo danh sách rỗng ta gán count = 0.
void InitList(L) {

L.count = 0;

Kiểm tra danh sách rỗng: EmptyList(L)


bool EmptyList(L) {

return (L.count == 0);

}
5
Lấy giá trị phần tử tại vị trí p trong danh sách: Value(p,L)

Trường hợp p<1 hoặc p>count báo “Lỗi: Không có vị trí này trong danh sách”.
Giá trị của phần tử tại vị trí p là element[p].
elementType Value(p,L) {

if ((p<1)||(p>L.count))

printf(“Lỗi: Không có vị trí này trong danh sách”);

else

return L.element[p];

Thêm một phần tử có giá trị x tại vị trí p của danh sách: InsertList (x,p,L)

Các khả năng có thể xảy ra khi thêm một phần tử vào danh sách:

- Mảng đầy (count = MaxLength): không thêm được.

- p<1, p>count+1: vị trí không tồn tại trong danh sách, không thực hiện được.

- 1pcount+1, ta thực hiện các thao tác:

o Độ dài danh sách tăng lên 1.

o Dời các phần tử từ vị trí p tới cuối danh sách xuống 1 vị trí.

o Gán giá trị tại vị trí p bằng x.

void InsertList(x,p,L) {

if (L.count == MaxLength) printf(“Danh sách đầy”);

else if ((p<1)||(p>count+1)) printf(“Không có vị trí này”);

else {

L.count = L.count + 1;

for(i = L.count, i>= p+1, i--)

L.element[i] = L.element[i-1];

L.element[p] = x;

}
6
Xóa một phần tử tại vị trí p của danh sách: DeleteList (p,L)

Các khả năng có thể xảy ra khi xóa một phần tử trong danh sách:

- p<1, p>count: vị trí không tồn tại trong danh sách, không thực hiện được.

- 1pcount, ta thực hiện các thao tác:

o Độ dài danh sách giảm đi 1.

o Dời các phần tử từ vị trí p tới cuối danh sách lên 1 vị trí.
void DeleteList(p,L) {

if ((p<1)||(p>count))

printf(“Không có vị trí này trong danh sách”);

else {

L.count = L.count - 1;

for (i = p, i<=L.count, i++) L.element[i] = L.element[i+1];

Định vị một phần tử trong danh sách có giá trị x: Locate (x,L)

Trả về 0 nếu không có phần tử nào có giá trị x. Trả về vị trí p của phần tử đầu
tiên trong danh sách có giá trị x.
int Locate(x,L) {

p = 1;

while ((p<=L.count)&&(L.element[p]!=x)) p = p+1;

if (p<=L.count) Then return p;

else return 0;

Liệt kê các phần tử của danh sách: PrintList(L)


void PrintList(L) {

for (p=1, i<=L.count, i++) printf(L.element[p]);

}
7
Ví dụ: Với danh sách sinh viên xây dựng ở trên, viết các chương trình con thực
hiện các yêu cầu sau:

1. Cài đặt các thao tác cơ bản trên danh sách sinh viên.

2. Định vị sinh viên có mã sinh viên là mid

3. Xóa sinh viên có mã sinh viên là mid.

4. Lập một danh sách sinh viên của lớp mclass

5. Hiển thị ra màn hình danh sách sinh viên của lớp mclass.

Hướng dẫn:
const int MaxLength = 50;

typedef

struct student{

char id[10];

char name[50];

char class[8];

};

struct ListST{

student element[MaxLength];

int count;

};

ListST L;

//Hiển thị thông tin của một sinh viên ra màn hình

void PrintStudent(s) {

printf(“\n %s,%s,%s”,s.id,s.name,s.class);

//Hiển thị danh sách sinh viên ra màn hình

void PrintList(L) {

for (p=1, p<=L.count, p++)

PrintStudent(L.element[p]);
8
}

//Định vị sinh viên có mã sinh viên là mid

int LocateID(mid,L) {

p = 1;

while ((p<=L.count)&& (strcmp(L.element[p].id,mid)!=0)) p++;

if (p<=L.count) return p;

else return 0;

//Xóa sinh viên có mã sinh viên là mid

void DeleteID(mid,L) {

p = LocateID(mid,L);

if (p==0)

printf(“Không có sinh viên này trong danh sách”);

else {

L.count = L.count - 1;

for (i = p, i<= L.count, i++)

L.element[i] = L.element[i+1];

//Lọc danh sách sinh viên của lớp mclass vào danh sách L1

void FilterClass(mclass,L1,L) {

InitList(L1); q = 1;

for (p=1, p<= L.count, p++)

if(strcmp(L.element[p].class,mclass)!=0) {

L1.element[q] = L.element[p];

q++;

L1.count = q – 1;

}
9
//Hiển thị danh sách sinh viên của lớp mclass

void PrintClass(mclass,L) {

FilterClass(mclass,L1,L);

PrintList(L1);

Yêu cầu về nhà: Sử dụng ngôn ngữ lập trình C cài đặt hoàn chỉnh danh sách
sinh viên và các thao tác cơ bản trên danh sách sinh viên.

3.3. DANH SÁCH LIÊN KẾT ĐƠN

3.3.1. Cấu trúc chứa con trỏ (cấu trúc tự trỏ)

Tổ chức lưu trữ móc nối (con trỏ tự trỏ) được thực hiện theo nguyên tắc:

- Mỗi phần tử của danh sách được lưu trữ trong một biến động (biến con trỏ),
còn gọi là node.

- Mỗi node có nhiều trường để lưu trữ thông tin (dữ liệu), ta gọi chung là
thành phần info.

- Mỗi node có một trường kiểu con trỏ lưu trữ địa chỉ của nút kế tiếp nó (nút
liền sau nó), ta gọi là thành phần next.

- Để cho đơn giản và thuận tiện trong biểu diễn giải thuật, ta nói nút có 2
trường là info và next. Thực tế, thành phần info có thể có nhiều trường mang
nhiều dữ liệu khác nhau.

node info next

3.3.2. Định nghĩa và sơ đồ cấu tạo danh sách liên kết đơn

Danh sách liên kết đơn là tập hợp các node, mỗi node là một biến động có hai
thành phần được kí hiệu là info và next. Thành phần info dùng để lưu thông tin (dữ
liệu) , thành phần next là trường kiểu con trỏ lưa địa chỉ của node đứng liền sau nó
(còn gọi là con trỏ trỏ đến node liền sau nó).
10
Node đầu tiên được quản lý bởi con trỏ F và node cuối cùng không có node
đứng liền sau nên giá trị trường next là NULL. Khi đó danh sách được gọi tên là
danh sách liên kết đơn L.

Quy ước danh sách F không có phần tử nào là danh sách rỗng, khi đó F =
NULL.

F Minh họa: p

NULL

Hình 4.2. Minh họa danh sách liên kết đơn

Vì mỗi node là một biến động nên ta có thể xin cấp phát khi cần và giải phóng
khi không cần đến nó.

3.3.3. Cài đặt danh sách liên kết đơn

typedef elementType...;

typedef struct element{

elementType info;

element *next;

typedef element *List;

List F;

Kiểu bản ghi element gồm hai thành phần info và next. Thành phần info có thể
có nhiều hơn một trường dữ liệu lưu trữ nhiều thông tin khác nhau. Thành phần next
là một con trỏ kiểu element.

Biến con trỏ F luôn luôn trỏ tới phần tử đầu tiên trong danh sách liên kết.

Ví dụ: Cài đặt danh sách liên kết quản lý sinh viên. Thành phần info gồm có 3
trường: id, name, class.

typedef struct student{

char [10] id;

char [50] name;

char [8] class;


11
student *next;

};

typedef student *List;

List F;

3.3.4. Các thao tác cơ bản trên danh sách liên kết đơn

Khởi tạo danh sách rỗng: InitList (F)

Danh sách rỗng trỏ về NULL.


void InitList(F) {

F = NULL;

Liệt kê các phần tử của danh sách: PrintList(F)


void PrintList(F) {

List p;

p = F;

while (p!=NULL) {

printf((*p).info); p = (*p).next;

Thêm một node có giá trị x vào đầu danh sách: InsertFirst (x,F)
void InsertFirst(x,F) {

List q;

q = new element; (*q).info = x; (*q).next = F;

F = q;

Thêm một node có giá trị x vào sau nút trỏ bởi p: Insert (x,p,F)
void Insert(x,p,F) {

List q;

q = new element;
12
(*q).info = x;

(*q).next = (*p).next;

(*p).next = q;

Xóa một phần tử tại vị trí p của danh sách: DeleteList (p,F)

Chỉ thực hiện xóa khi danh sách khác rỗng (F!=NULL) và nút p có ở trong danh
sách.

- p là nút đầu danh sách (p==F): thay đổi vị trí nút đầu danh sách F =
(*F).next.

- p không phải là nút đầu: tìm tới nút q là nút trước nút p và thực hiện phép
gán (*q).next = (*p).next.

- Thực hiện giải phóng bộ nhớ trỏ bởi p.


void DeleteList(p,F) {

List q;

if (F!=NULL) {

if(F==p) F = (*F).next;

else{

q = F;

while((q!=NULL)&&((*q).next!=p)) q = (*q).next;

If(q!=NULL) (*q).next = (*p).next;

delete p;

Đếm số phần tử của danh sách: Count (F)


int Count(F) {

int c = 0;

List p;
13
p = F;

while(p!=NULL) {

c++;

p = (*p).next;

return c;

Định vị phần tử có giá trị x trong danh sách: Locate (x,F)

Hàm trả về địa chỉ nút đầu tiên trong danh sách có giá trị x. Nếu không tìm thấy
hàm trả về NULL
List Locate(x,F) {

List p;

p = F;

while((p!=NULL)&&((*p).info!=x)) p = (*p).next;

return p;

Thêm một nút có giá trị x vào danh sách đã sắp xếp: InsertSort (x,F)

Giả sử danh sách F sắp xếp theo thứ tự tăng dần của trường info.

Tìm vị trí nút đầu tiên có giá trị lớn hơn hoặc bằng x (nút q) và nút trước nút q
(nút p).

o Nếu q == F thì thêm vào đầu danh sách

o Ngược lại, thêm vào sau nút p, trước nút q .


void InsertSort(x,F) {

List p,q;

q = F;

while((q!=NULL)&&(*q).info<x) {

p = q; q = (*q).next;

}
14
if(q==F) InsertFirst(x,F);

else Insert(x,p,F);

3.3.5. Ví dụ

Ví dụ 1. Cài đặt danh sách liên kết quản lý sinh viên. Thành phần info gồm
có 3 trường: id, name, class và các thao tác sau:

1. Định vị sinh viên có mã sinh viên là mid

2. Xóa sinh viên có mã sinh viên là mid.

3. Hiển thị danh sách sinh viên.

4. Đếm số sinh viên của danh sách.


typedef struct student {

char [10] id;

char [50] name;

char [8] class;

student *next;

};

typedef student *List;

List F;

//Định vị sinh viên có mã là mid

List LocateID(mid,F) {

List p;

p = F;

while((p!=NULL)&&(strcmp((*p).id,mid)!=0)) p = (*p).next;

return p;

//Xóa sinh viên có mã là mid: Định vị sinh viên có mã mid và xóa.

void DeleteList(p,F) { //Xóa nút trỏ bởi p trong danh sách

List q,r;
15
r = F;

while((r!=NULL)&&(r!=p)) {

q = r; r = (*r).next;

if(r!=NULL) {

if(F==p) F = (*F).next;

else (*q).next = (*p).next;

delete p;

void DeleteID(mid,F) { //Xóa nút có mã là mid

List p;

p = LocateID(mid,F)

DeleteList(p,F);

//Hiển thị danh sách sinh viên

void PrintStudent(p) { //Hiển thị thông tin của một sinh

printf(“\n %s,%s,%s”, (*p).id, (*p).name, (*p).class);

void PrintList(F) { //Hiển thị danh sách sinh viên

List p;

p = F;

while (p!=NULL) {

PrintStudent(p);

p = (*p).next;

//Tìm và hiển thị các sinh viên của lớp mlass

void PrintClass(mclass,F) {
16
List p;

p = F;

while(p!=NULL) {

if(strcmp((*p).class,mclass)==0)) PrintStudent(p);

p = (*p).next;

//Đếm số sinh viên trong danh sách

int Count(F) {

int c = 0;

List p;

p = F;

while(p!=NULL) {

c++;

p = (*p).next;

return c;

Yêu cầu về nhà: Sử dụng ngôn ngữ lập trình C cài đặt hoàn chỉnh danh sách
sinh viên và các thao tác cơ bản trên danh sách sinh viên.

Ví dụ 2. Bài toán xử lý đa thức.

Cho đa thức P(x) = anxn+ an-1xn-1+...+ a1x + a0. Xây dựng cấu trúc dữ liệu thích
hợp để biểu diễn đa thức và cài đặt một số thao tác xử lý trên đa thức như sau:

- Thêm một phần tử vào cuối đa thức.

- Hiển thị đa thức ra màn hình.

- Tính giá trị của P(x) khi biết x.

- Tính tổng của hai đa thức.

Hướng dẫn:
17
Lưu trữ đa thức dưới dạng một danh sách liên kết đơn có nút đầu danh sách trỏ
bởi P, mỗi nút của danh sách lưu một đơn thức (chỉ lưu các đơn thức có hệ số khác
0), có 3 trường như sau:

coef exp next

Trong đó:

- Trường coef lưu hệ số khác 0 của đơn thức.

- Trường exp lưu bậc (số mũ) của đơn thức.

- Trường next lưu địa chỉ của nút kế tiếp.

* Tính giá trị của P(x) khi biết x.

- Biến sum = 0

- Duyệt qua các nút của đa thức và thực hiện các phép tính: sum = sum +
coef*pow(x,exp).

* Tính tổng của hai đa thức P(x), Q(x).

Các đơn thức trong các nút được lưu theo dạng lũy thừa lùi (đơn thức có bậc
giảm dần). Giả sử đã có 2 đa thức P(x) và Q(x) được lưu trong hai danh sách móc
nối đơn P và Q theo cách trên, đa thức tổng R(x) = P(x) + Q(x) và đa thức tổng được
lưu trong danh sách móc nối đơn R.

Chẳng hạn:

P(x) = 7x5 + 3x4 + 4x2 + x + 6

Q(x) = 2x4 + 5x3 + 3x

R(x) = P(x) + Q(x) = 7x5 + 5x4 + 5x3 + 4x2 + 4x + 6

Ý tưởng giải thuật:

- Dùng con trỏ p, q để duyệt các nút của danh sách P, Q.

- Chừng nào p và q còn khác NULL thì thực hiện một trong 3 cách sau:
18
o Nếu bậc của nút trỏ bởi p lớn hơn bậc của nút trỏ bởi q
((*p).exp>(*q).exp) thì: bổ sung một nút mới vào cuối danh sách R,
chép dữ liệu từ nút trỏ bởi p vào nút này, di chuyển p tới nút tiếp theo.

o Nếu bậc của nút trỏ bởi p nhỏ hơn bậc của nút trỏ bởi q
((*p).exp<(*q).exp) thì: bổ sung một nút mới vào cuối danh sách R,
chép dữ liệu từ nút trỏ bởi q vào nút này, di chuyển q tới nút tiếp theo.

o Nếu bậc của nút trỏ bởi p bằng hơn bậc của nút trỏ bởi q
((*p).exp=(*q).exp) thì: tính tổng hệ số trên nút trỏ bởi p và q, nếu
tổng này khác không thì bổ sung một nút mới vào cuối danh sách R
với hệ số là tổng hệ số và bậc là bậc của nút p (hoặc q). Di chuyển p
và q tới nút tiếp theo.

- Cuối cùng, nếu đa thức nào còn đơn thức chưa xử lý thì sao chép chúng vào
cuối danh sách R.

Sau đây là khai báo đa thức và một số giải thuật xử lý đa thức:

typedef struct polynomical{

double coef;

int exp;

polynomical *next;

};

typedef polynomical *List;

List P, Q, R;

//Thêm một phần tử vào sau vị trí p trong danh sách

void Insert(c,e,p,P) {

List q;

q = new polynomical;

(*q).coef = c; (*q).exp = e;

if(P==NULL) {(*q).next = NULL; P = q;}

else{(*q).next = (*p).next; (*p).next = q;}

}
19
//Tính P(x) khi biết x

double Px(x,P) {

double sum = 0;

List p;

p = P;

while(p!=NULL) {

sum = sum + (*p).coef*pow(x,(*p).exp);

p = (*p).next;

return sum;

//Tính tổng hai đa thức

List Totalize(P,Q) {

List p,q,r;

double c = 0;

int e;

R = NULL; p = P; q = Q; r = R;

while((p!=NULL)||(q!=NULL)) {

if((*p).exp>(*q).exp) {

c = (*p).coef; e = (*p).exp;

p = (*p).next;

}else if((*p).exp<(*q).exp) {

c = (*q).coef; e = (*q).exp;

q = (*q).next;

}else{ c = (*p).coef + (*q).coef; e = (*p).exp;

p = (*p).next; q = (*q).next;

if(c!=0) {

Insert(c,e,r,R);
20
if(r==NULL) r = R; else r = (*r).next;

while(p!=NULL) {

Insert((*p).coef,(*p).exp,r,R);

if(r==NULL) r = R; else r = (*r).next;

while(q!=NULL) {

Insert((*q).coef,(*q).exp,r,R);

if(r==NULL) r = R; else r = (*r).next;

return R;

3.4. CÁC DẠNG KHÁC CỦA DANH SÁCH LIÊN KẾT

3.4.1. Danh sách nối vòng

Từ một danh sách móc nối đơn F ta cải tiến lại như sau: trường next của nút
cuối cùng đang trỏ tới NULL bây giờ cho lưu địa chỉ của nút đầu tiên của danh sách
(nút trỏ bởi F). Danh sách được cải tiến như vậy được gọi là danh sách nối vòng.

Hình 3.3. Minh họa danh sách móc nối vòng

Dưới đây là các giải thuật cơ bản trên danh sách móc nối vòng:

Hiển thị danh sách: PrintList(F)

Danh sách chỉ được hiển thị khi khác NULL. Hiển thị danh sách cho tới nút
cuối cùng (có trường next == F).
void PrintList(F) {

List p;
21
if(F!=NULL) {

p = F;

do{

printf((*p).info);

p = (*p).next;

} while (p != F)

Thêm một node có giá trị x vào đầu danh sách: InsertFirst (x,F)
void InsertFirst(x,F) {

List q,p;

q = new element;

(*q).info = x;

if(F==NULL) {

F = q;

(*q).next = F;

}else{ p = F

while((*p).next!=F) p = (*p).next;

(*p).next = q;

(*q).next = F;

F = q;

Thêm một node có giá trị x vào sau nút trỏ bởi p: Insert (x,p,F)
void Insert(x,p,F) {

List q;

q = new element;

(*q).info = x; (*q).next = (*p).next; (*p).next = q;

}
22
Xóa một phần tử tại vị trí p của danh sách: DeleteList (p,F)

Chỉ thực hiện xóa khi danh sách khác rỗng (F!=NULL) và nút p có ở trong danh
sách.

- p là nút đầu danh sách (p==F):

o F chỉ có 1 nút: F = NULL

o F có hơn 1 nút: thay đổi vị trí nút đầu danh sách F = (*F).next và nút
cuối danh sách trỏ tới F mới.

- p không phải là nút đầu: tìm tới nút q là nút trước nút p và thực hiện phép
gán (*q).next = (*p).next.

- Thực hiện giải phóng bộ nhớ trỏ bởi p.

void DeleteList(p,F) {

List q;

if (F!=NULL) {

if(F==p)

if(F==(*F).next) F = NULL;

else{ q = F;

while((*q).next != F) q = (*q).next;

(*q).next = (*F).next;

F = (*F).next;

else{

q = F;

while(((*q).next!=F)&&((*q).next!=p)) q = (*q).next;

If((*q).next==p) (*q).next = (*p).next;

delete p;

}
23
Đếm số phần tử của danh sách: Count (F)
int Count(F) {

int c = 0;

List p;

p = F;

if(F != NULL){

do {

c++;

p = (*p).next;

}while ((*p).next != F);

return c;

Định vị phần tử có giá trị x trong danh sách: Locate (x,F)

Hàm trả về địa chỉ nút đầu tiên trong danh sách có giá trị x. Nếu không tìm thấy
hàm trả về NULL
List Locate(x,F) {

List p;

p = F;

if (F!=NULL)

do

if((*p).info!=x) p = (*p).next;

while((p!=L)&&((*p).info!=x));

return p;

3.4.2. Danh sách liên kết đôi

Danh sách liên kết đôi là tập hợp các nút, mỗi nút là một biến động có tối thiểu
3 thành phần được kí hiệu là info, pre, next. Trong đó:
24
- info lưu trữ thông tin.

- pre là con trỏ lưu nút đứng trước nó.

- next là con trỏ lưu nút đứng sau nó.

Nút đầu tiên được quản lý bởi con trỏ F (First), không có nút đứng trước: pre =
NULL.

Nút cuối cùng được quản lý bởi con trỏ L (Last), không có nút đứng sau: next
= NULL.
L
F

NULL
NULL

Hình 4.3. Danh sách liên kết đôi

Cài đặt danh sách liên kết đôi và các thao tác trên danh sách liên kết đôi:
typedef elementType...;

struct element{

elementType info;

element *pre, *next;

typedef element *List;

List F,L;

Khởi tạo danh sách rỗng: InitList (F,L)

Danh sách rỗng trỏ về NULL.


void InitList(F,L) {

F = NULL; L = NULL;

Liệt kê các phần tử của danh sách: PrintList(F,L)


void PrintList(F,L) {

List p;

p = F;
25
while (p!=NULL) {

printf((*p).info);

p = (*p).next;

Thêm một node có giá trị x vào đầu danh sách: InsertFirst (x,F,L)
void InsertFirst(x,F,L) {

List q;

q = new element;

(*q).info = x;

(*q).pre = NULL;

(*q).next = F;

(*F).pre = q;

F = q;

Thêm một node có giá trị x vào cuối danh sách: InsertLast (x,F,L)
void InsertLast(x,F,L) {

List q;

q = new element;

(*q).info = x;

(*q).next = NULL;

(*q).pre = L;

(*L).pre = q;

L = q;

Thêm một node có giá trị x vào sau nút trỏ bởi p: InsertFirst (x,p,F,L)
void Insert(x,p,F,L) {

List q;

q = new element;
26
(*q).info = x;

(*q).next = (*p).next;

(*q).pre = p;

(*(*p).next).pre = q;

(*p).next = q;

Xóa một phần tử tại vị trí p của danh sách: DeleteList (p,F,L)

Chỉ thực hiện xóa khi danh sách khác rỗng (F!=NULL)&&(L!=NULL) và nút
p có ở trong danh sách. Các trường hợp có thể xảy ra:

- p là nút đầu danh sách (p==F):

o Danh sách có 1 nút (F ==L): F = NULL, L = NULL

o Danh sách có hơn 1 nút: F = (*F).next, (*F).pre = NULL

- p là nút cuối danh sách (p==L): L = (*L).pre; (*L).next = NULL

- p nằm giữa danh sách (có nút trước và nút sau p):

(*(*p).pre).next = (*p).next; (*(*p).next).pre = (*p).pre;

- Thực hiện giải phóng bộ nhớ trỏ bởi p.


void DeleteList(p,F,L) {

List q;

if ((F!=NULL)&&(L!=NULL)&&(p!=NULL)) {

if(F==p)

if(F == L){ F = NULL; L = NULL;}

else { F = (*F).next; (*F).pre = NULL;}

else if(p==L) {

L = (*L).pre; (*L).next = NULL;}

else{

q = F;

while((q!=NULL)&&((*q).next!=p)) q = (*q).next;
27
if(q!=NULL) { //p có trong danh sách

(*q).next = (*p).next;

(*(*p).next).pre = q;s

delete p;

Đếm số phần tử của danh sách: Count (F,L)


int Count(F) {

int c = 0;

List p;

p = F;

while(p!=NULL) {c++; p = (*p).next;}

return c;

Định vị phần tử có giá trị x trong danh sách: Locate (x,F,L)

Hàm trả về địa chỉ nút đầu tiên trong danh sách có giá trị x. Nếu không tìm thấy
hàm trả về NULL
List Locate(x,F,L) {

List p;

p = F;

while((p!=NULL)&&((*p).info!=x)) p = (*p).next;

return p;

3.5. NGĂN XẾP VÀ HÀNG ĐỢI


28
Danh sách tuyến tính là một cấu trúc dữ liệu được sử dụng rộng rãi trong việc
giải các bài toán tin học. Với danh sách tuyến tính thì việc bổ sung hay loại bỏ các
phần tử có thể thực hiện tại bất cứ đối tượng nào tùy theo nhu cầu thực tế.

Hai trong các danh sách đặc biệt theo một quan điểm riêng về bổ sung và loại
bỏ ứng là ngăn xếp (stack) và hàng đợi (queue). Ngăn xếp được thực hiện bổ sung
và loại bỏ theo nguyên tắc vào sau ra trước (LIFO – Last In First Out). Hàng đợi
thực hiện bổ sung và loại bỏ theo nguyên tắc vào trước ra trước (FIFO – First In First
Out).

Như vậy, ngăn xếp và hàng đợi đều là cách danh sách tuyến tính mà trên đó
chúng ta tạo ra các cơ chế hoạt động riêng cho chúng trong việc bổ sung và loại bỏ.

3.5.1. Ngăn xếp (Stack) và các thao tác cơ bản trên các ngăn xếp

Stack là một cấu trúc dữ liệu trên đó đã được trang bị cấu trúc dữ liệu tuyến
tính, hoạt động theo nguyên tắc:

- Mỗi lần nộp vào (bổ sung) hoặc lấy ra (loại bỏ) chỉ thực hiện với một phần
tử.

- Phần tử nào nộp vào sau khi lấy ra sẽ được lấy ra trước.

Để đảm bảo cơ chế đó thì các phần tử của stack tốt nhất được tổ chức theo cấu
trúc danh sách tuyến tính. Vị trí nộp vào và lấy ra sẽ được chọn là vị trí đầu tiên
(hoặc vị trí cuối cùng) của danh sách, vị trí đó được gọi là đỉnh của stack, phía ngược
lại được gọi là đáy của stack.

Stack rỗng: khi không có phần tử nào lưu trữ trong stack.

Stack đầy: khi không còn chỗ nộp thêm phần tử mới vào stack.

3.5.2. Mô hình hóa ngăn xếp và cài đặt ngăn xếp

Chúng ta có thể sử dụng danh sách đặc (mảng một chiều) hoặc danh sách liên
kết để tổ chức stack.

* Tổ chức stack trên mảng một chiều (lưu trữ kế tiếp).


29
Dùng mảng một chiều có n phần tử S[n] để tổ chức stack. Đáy của stack là vị
trí số 1 của mảng S, biến top để quản lý đỉnh của stack.
const int MaxLength = ...;

typedef

elementType ...; //Định nghĩa kiểu phần tử của stack

struct Stack {

elementType element[MaxLength+1];

int top;

};

Stack L;

Stack đầy khi top = MaxLength. Stack rỗng khi top = 0.

Bổ sung một phần tử x vào stack

- Kiểm tra xem stack có đầy không (top = MaxLength). Nếu không đầy thì
dịch chuyển top lên một vị trí và nộp x vào vị trí này.
void InsertStack(x,S) {

if(S.top == MaxLength) printf(“Stack đầy”);

else { S.top = S.top +1; S.element[top] = x;}

Lấy một phần tử ra khỏi stack

- Nếu stack khác rỗng (top>0) thì mới lấy. Sau khi lấy, đỉnh stack lùi lại một
vị trí.

elementType DeleteStack(S) {

if(S.top ==0) printf(“Stack rỗng”)

else { S.top = S.top -1; return(S.element[top+1]);}

* Tổ chức stack trên danh sách liên kết đơn (lưu trữ móc nối).
30
Dùng một danh sách móc nối đơn có nút đầu danh sách trỏ bởi S và tổ chức
stack trên đó. Ở đây ta chọn vị trí đầu tiên của danh sách là đỉnh và vị trí cuối cùng
của danh sách là đáy.

Stack rỗng khi S = NULL.

Stack đầy tùy thuộc vào bộ nhớ và cấu hình của máy, đứng về góc độ lý thuyết
ta giả sử không xảy ra hiện tượng stack đầy.
typedef elementType...;

struct element{

elementType info;

element *next;

typedef element *Stack;

Stack S;

Bổ sung một phần tử x vào stack: bổ sung một phần tử vào đầu danh sách
(InsertFirst)
void InsertStack(x,S){}

Lấy một phần tử ra khỏi stack: lấy phần tử đầu tiên trong danh sách

elementType DeleteStack(S) {

Stack p; elementType x;

if(S==NULL) printf(“Stack rỗng”)

else { p = S; x = (*p).info;

S = (*S).next;

delete p;

return x;

}
31
3.5.3. Hàng đợi (Queue) và các thao tác cơ bản trên các hàng đợi

Queue là một cấu trúc lưu trữ trên đó trang bị một cấu trúc dữ liệu tuyến tính,
hoạt động theo nguyên tắc:

- Mỗi lần nộp vào hoặc lấy ra chỉ thực hiện với một phần tử.

- Phần tử nộp vào trước sẽ được lấy ra trước.

Vị trí nộp phần tử vào là vị trí đầu tiên (hoặc cuối cùng) và vị trí lấy ra sẽ là vị
trí cuối cùng (hoặc đầu tiên) của danh sách. Vị trí nộp phần tử vào được gọi là lối
vào (hay lối sau), vị trí lấy phần tử ra được gọi là lối ra (hay lối trước).

Queue rỗng khi không có phần tử nào trong Queue và Queue đầy khi không
còn chỗ để nộp phần tử vào.

3.5.4. Mô hình hóa hàng đợi và cài đặt hàng đợi

Ta tổ chức Queue trên một danh sách móc nối đơn có nút đầu tiên trỏ bởi Q,
nút cuối cùng trỏ bởi R. Lối vào của Queue là vị trí cuối cùng (R), lối ra là vị trí đầu
tiên của danh sách (Q). Queue rỗng khi danh sách rỗng: Q=R=NULL, Queue đầy
tùy thuộc vào bộ nhớ và cấu hình máy, giả sử không xảy ra hiện tượng Queue đầy.
typedef elementType...;

struct element{

elementType info;

element *next;

typedef element *Queue;

Queue Q,R;

Bổ sung một phần tử x vào queue: bổ sung một phần tử vào cuối danh sách
void InsertQueue(x,Q,R) {

Queue p;

p = new element;

(*p).info = x; (*p).next = NULL;

if(Q==NULL){Q = p; R = p;}
32
else {(*R).next = p; R = p;}

Lấy một phần tử ra khỏi queue: lấy phần tử đầu tiên trong danh sách
elementType Delete Queue(Q,R) {

Queue p; elementType x;

if(Q==NULL) printf(“Queue rỗng”)

else { p = Q; x = (*p).info;

if(Q==R){Q = NULL; R = NULL;}

else Q = (*Q).next;

delete p;

return x;

}
33
BÀI TẬP CHƯƠNG 4

Bài 1. So sánh ưu điểm, nhược điểm của danh sách kề và danh sách móc nối.

Bài 2. Cho trước một danh sách móc nối đơn F, mỗi nút chứa một số nguyên,
danh sách này chưa có thứ tự. Viết giải thuật để tạo một danh sách móc nối đơn mới
từ FS từ F sao cho FS có thứ tự không giảm.

Bài 3. Cho một danh sách móc nối đơn F, mỗi nút chứa một số nguyên. Hãy
viết các giải thuật giải quyết các công việc sau:

1. Tính số lượng các nút của danh sách.

2. Tìm tới nút thứ k trong danh sách, nếu có thì cho ra địa chỉ của nút đó, nếu
không có trả về NULL.

3. Bổ sung nút mới chứa số nguyên x vào sau nút thứ k, nếu không có nút thứ
k thì bổ sung vào sau cùng.

4. Loại bỏ nút đứng trước nút thứ k.

5. Cho một danh sách móc nối đơn có nút đầu trỏ bởi P. Hãy chèn danh sách P
vào sau nút trỏ bởi M trên danh sách F.

6. Đảo ngược danh sách F đã cho.

7. Nếu trong danh sách F còn có hai nút chứa số nguyên bằng nhau thì loại bỏ
bớt một nút.

8. Trên cơ sở danh sách đã xử lý ở câu 7, cài đặt các thuật toán sắp xếp sau trên
danh sách F:

a. Chọn trực tiếp - Selection Sort

b. Chèn trực tiếp - Insertion Sort

c. Binary Insertion Sort

d. Ðổi chỗ trực tiếp - Interchange Sort

e. Nổi bọt - Bubble Sort

f. Quick Sort
34
g. Merge Sort

Bài 4. Cài đặt lại bài 3 trên danh sách nối vòng và danh sách liên kết đôi.

Bài 5. Có một nhóm người đang đứng thành một vòng tròn, người ta cần chọn
ra một người đi dự tiệc theo nguyên tắc sau:

- Cho trước một số nguyên n và chọn ngẫu nhiên một người, giả sử người đó
tên A.

- Bắt đầu từ người A, theo chiều kim đồng hồ, bắt đầu đếm từ 1 đến n. Người
đếm thứ n được đưa ra khỏi vòng. Lặp lại quy trình đếm đó bắt đầu từ người
tiếp theo cho đến khi chỉ còn một người.

- Người còn lại cuối cùng là người được chọn đi dự tiệc.

Hãy tổ chức cấu trúc dữ liệu phù hợp và viết các giải thuật giải quyết các công
việc sau:

a. Chọn ra người đi dự tiệc.

b. Nếu có một người tên A rất muốn đi dự tiệc, hãy giúp A chỉ ra người chọn
đầu tiên sao cho A là người cuối cùng còn lại sau khi đã loại hết mọi người
khỏi vòng.

Bài 6. Một trung tâm đào tạo tin học cần quản lý việc học của học viên theo
chứng chỉ môn học. Trong mỗi kỳ, trung tâm lưu trữ thông tin về kết quả học tập
của các học viên trong một danh sách liên kết với nút đầu trỏ bởi F. Mỗi nút của
danh sách lưu các thông tin:

- hoten: lưu họ tên học viên (trường khóa).

- dsMon: lưu địa chỉ nút đầu của một danh sách khác chứa các kết quả các
môn học mà học viên này đã đăng kí trong học kì đó (gọi là danh sách môn
học).

- down: con trỏ lưu địa chỉ nút tiếp theo trong danh sách học viên.

Mỗi nút trong danh sách môn học gồm các trường:

- tenmh: lưu tên môn học (trường khóa).


35
- diem: lưu điểm của môn học

- next: con trỏ trỏ tới nút kế tiếp trong danh sách môn học.

Tổ chức dữ liệu và viết các giải thuật xử lý các công việc sau:

a. Tìm địa chỉ nút trong danh sách học viên F có hoten là ht.

b. Tìm địa chỉ nút trong danh sách môn học P có tenmh là mh.

c. Bổ sung một học viên có hoten là ht vào danh sách F và học viên này có
danh sách môn học ban đầu gồm 1 môn có tên là mh. Nếu học viên này đã
có thì bổ sung môn học có tên mh vào cuối danh sách môn học của học viên
này.

d. Cập nhật điểm thi dt của môn học mh cho học viên ht. Nếu không tìm thấy
học viên ht thì bổ sung học viên ht và môn học mh kèm số điểm dt. Nếu tìm
thấy học viên ht nhưng không tìm thấy môn học mh thì bổ sung môn học mh
kèm số điểm dt vào danh sách môn học của học viên này.

e. In danh sách học viên và điểm của từng môn kèm điểm trung bình (tổng
điểm chia cho số môn học).s

You might also like