Tải bản đầy đủ (.pdf) (38 trang)

Tài liệu Mảng và danh sách pptx

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 (543.48 KB, 38 trang )

Bài 10: Mảng và danh sách
10.1. Mảng
10.1.1. Mảng một chiều, mảng nhiều chiều
Kh¸i niƯm
Mảng là một tập hợp các phần tử cố định có cùng một kiểu, gọi là kiểu
phần tử. Kiểu phần tử có thể là có các kiểu bất kỳ: ký tự, số, chuỗi ký tự…; cũng
có khi ta sử dụng kiểu mảng để làm kiểu phần tử cho một mảng (trong trường hợp
này ta gọi là mảng của mảng hay mảng nhiều chiều).
Ta có thể chia mảng làm 2 loại: mảng 1 chiều và mảng nhiều chiều.
Mảng là kiểu dữ liệu được sử dụng rất thường xuyên. Chẳng hạn người ta
cần quản lý một danh sách họ và tên của khoảng 100 sinh viên trong một
lớp. Nhận thấy rằng mỗi họ và tên để lưu trữ ta cần 1 biến kiểu chuỗi, như
vậy 100 họ và tên thì cần khai báo 100 biến kiểu chuỗi. Nếu khai báo như
thế này thì đoạn khai báo cũng như các thao tác trên các họ tên sẽ rất dài
dòng và rắc rối. Vì thế, kiểu dữ liệu mảng giúp ích ta trong trường hợp
này; chỉ cần khai báo 1 biến, biến này có thể coi như là tương đương với
100 biến chuỗi ký tự; đó là 1 mảng mà các phần tử của nó là chuỗi ký tự
CÊu tróc l-u trữ của mảng.
Cấu trúc dữ liệu đơn giản nhất dùng địa chỉ tính đ-ợc
để thực hiện l-u trữ và tìm kiếm phần tử, là mảng một
chiều hay véc tơ.
Thông th-ờng thì một số từ máy sẽ đ-ợc dành ra để
l-u trữ các phần tử của mảng. Cách l-u trữ này đ-ợc gọi
l-u
trữ
kế
tiếp
(sequential
storage

cách


allocation).
Tr-ờng hợp một mảng một chiều hay véc tơ có n phần
tử của nó có thể l-u trữ đ-ợc trong một từ máy thì cần
phải dành cho nó n từ máy kế tiếp nhau. Do kích th-ớc
của véc tơ đà đ-ợc xác định nên không gian nhớ dành ra
cũng đ-ợc ấn định tr-ớc.
Véc tơ A có n phần tử, nếu mỗi phần tử ai (0 i
n) chiếm c từ máy thì nó sẽ đ-ợc l-u trữ trong cn từ
máy kế tiếp nh- hình vẽ:
a0

A1

. .
ai
. .
.
.
cn từ máy kế tiếp nhau

an

L0
Địa chỉ của ai đ-ợc tính bởi công thức:
Loc(ai) = L0 + c * i
trong đó :
L0 đ-ợc gọi là địa chỉ gốc - đó là địa chỉ từ máy
đầu tiên trong miền nhớ kế tiếp dành để l-u trữ véc tơ
(gọi là véc tơ l-u trữ).



f(i) = c * i gọi là hàm địa chỉ (address
function)
Đối với mảng nhiều chiều việc l-u trữ cũng t-ơng
tự nh- vậy nghĩa là vẫn sử dụng một véc tơ l-u trữ kế
tiếp nh- trên.
a11

a01

. .
.

aij

. .
.

anm

Giả sử mỗi phần tử trong ma trận n hàng m cột
(mảng nhiều chiều) chiếm một từ máy thì địa chỉ của aij
sẽ đ-ợc tính bởi công thức tổng quát nh- sau:
Loc(aij) = L0 + j * n + i { theo thø tù -u tiªn cét
(column major order }
Cịng víi ma trËn n hàng, m cột cách l-u trữ theo
thứ tự -u tiên hàng (row major order) thì công thức
tính địa chỉ sẽ lµ:
Loc(aij) = L0 + i * m + j
+ Tr-êng hợp cận d-ới của chỉ số không phải là 1, nghĩa

là ứng với aij thì b1 i u1, b2 j u2 thì ta sẽ có
công thức tính địa chỉ nh- sau:
Loc(aij) = L0 + (i - b1) * (u2 - b2 + 1) + (j - b2)
vì mỗi hàng có (u2 - b2 + 1) phần tử.
Ví dụ : Xét mảng ba chiều B có các phÇn tư bijk víi 1 ≤
i ≤ 2;
1 ≤ j 3; 1 k 4; đ-ợc l-u trữ theo thứ tự -u
tiên hàng thì các phần tử của nó sẽ đ-ợc sắp đặt kế
tiếp nh- sau:
b111, b112, b113, b114, b121, b122, b123, b124, b131, b132, b133,
b134, b211, b212, b213, b214, b221, b222, b223, b224, b231, b232, b233,
b234.
C«ng thøc tính địa chỉ sẽ là :
Loc(aijk) = L0 + (i - 1) *12 + (j - 1) * 4 + (k - 1)
VD Loc(b223) = L0 + 22.
XÐt tr-êng hỵp tổng quát với mảng A n chiều mà các
phần tử là :
A[s1, s2, . . ., sn] trong đó bi ≤ si ≤ ui ( i = 1, 2, . .
., n), ứng với thứ tự -u tiên hàng ta cã:
n

Loc(A[s1, s2, . . ., sn]) = L0 + ∑ pi(si - bi)
n

i=1

víi pi = Π (uk - bk +1)
k =i + 1

đặc biệt pn = 1.

Chú ý :


1> Khi mảng đ-ợc l-u trữ kế tiếp thì việc truy
nhập vào phần tử của mảng đ-ợc thực hiện trực
tiếp dựa vào địa chỉ tính đ-ợc nên tốc độ nhanh
và đồng đều đối với mọi phần tử.
2> Mặc dầu có rất nhiều ứng dụng ở đó mảng có thể
đ-ợc sử dụng để thể hiện mối quan hệ về cấu trúc
giữa các phần tử dữ liệu, nh-ng không phải không
có những tr-ờng hợp mà mảng cũng lộ rõ những
nh-ợc điểm của nó.
Ví dụ : Xét bài toán tính đa thức của x,y chẳng hạn
cộng hai đa thức 2 sau:
(3x - xy + y2 + 2y - x)
+ (x2 + 4xy - y2 +2x)
=
(4x2 + 3xy + 2y + x)
Ta biÕt khi thực hiện cộng 2 đa thức ta phải phân
biệt đ-ợc từng số hạng, phân biệt đ-ợc các biến, hệ số
và số mũ.
Để biểu diễn đ-ợc một đa thức với 2 biÕn x,y ta cã
thĨ dïng ma trËn: hƯ sè cđa số hạng xiyj sẽ đ-ợc l-u trữ
ở phần tử có hàng i cột j của ma trận. Nếu ta hạn chế
kích th-ớc của ma trận là n ì n thì số mũ cao nhất của
x,y chỉ xử lý đ-ợc
với đa 2 thøc bËc n-1 th«i.
2
VD : Víi x + 4xy - y +2x th× ta sÏ sư dơng ma
trËn 5 × 5 biĨu diƠn nã sÏ cã d¹ng:


0
1
2
3
4

0
0
0
0
0

0
2
0
0
0

-1
4
1
0
0

0
0
0
0
0


0

1

2

3

4

10.1.2. Cấu trúc lưu trữ mảng trên một số ngơn ngữ lập trình
I. GIỚI THIỆU KIỂU DỮ LIỆU “KIỂU MẢNG” TRONG C
. Hay như để lưu trữ các từ khóa của ngơn ngữ lập trình C, ta cũng dùng
đến một mảng để lưu trữ chúng.
Ví dụ 1: Viết chương trình cho phép nhập 2 ma trận a, b có m dịng n cột,
thực hiện phép toán cộng hai ma trận a,b và in ma trận kết quả lên màn hình.
Trong ví dụ này, ta sẽ sử dụng hàm để làm ngắn gọn hơn chương trình của
ta. Ta sẽ viết các hàm: nhập 1 ma trận từ bàn phím, hiển thị ma trận lên màn hình,
cộng 2 ma trận.


#include<conio.h>
#include<stdio.h>
void Nhap(int a[][10],int M,int N)
{
int i,j;
for(i=0;iTrang 76 Lập trình căn bản
for(j=0; j

printf("Phan tu o dong %d cot %d: ",i,j);
scanf("%d",&a[i][j]);
}
}
void InMaTran(int a[][10], int M, int N)
{
int i,j;
for(i=0;ifor(j=0; j< N; j++)
printf("%d ",a[i][j]);
printf("\n");
}
}
/* Cong 2 ma tran A & B ket qua la ma tran C*/
void CongMaTran(int a[][10],int b[][10],int M,int N,int c[][10]){
int i,j;
for(i=0;ifor(j=0; jc[i][j]=a[i][j]+b[i][j];
}
int main()
{
int a[10][10], b[10][10], M, N;
int c[10][10];/* Ma tran tong*/
printf("So dong M= "); scanf("%d",&M);
printf("So cot M= "); scanf("%d",&N);
printf("Nhap ma tran A\n");
Nhap(a,M,N);
printf("Nhap ma tran B\n");
Nhap(b,M,N);

printf("Ma tran A: \n");
InMaTran(a,M,N);
printf("Ma tran B: \n");
InMaTran(b,M,N);
CongMaTran(a,b,M,N,c);


printf("Ma tran tong C:\n");
InMaTran(c,M,N);
getch();
return 0;
}

Mảng trong C#
Array là một cấu trúc dữ liệu cấu tạo bởi một số biến được gọi là những phần tử
mảng. Tất cả các phần tử này đều thuộc một kiểu dữ liệu. Bạn có thể truy xuất
phần tử thông qua chỉ số (index). Chỉ số bắt đầu bằng zero.
Có nhiều loại mảng (array): mảng một chiều, mảng nhiều chiều.
Cú pháp :
type[ ] array-name;
thí dụ:
int[] myIntegers; // mảng kiểu số nguyên
string[] myString ; // mảng kiểu chuổi chữ
Bạn khai báo mảng có chiều dài xác định với từ khoá new như sau:
// Create a new array of 32 ints
int[] myIntegers = new int[32];
integers[0] = 35;// phần tử đầu tiên có giá trị 35
integers[31] = 432;// phần tử 32 có giá trị 432
Bạn cũng có thể khai báo như sau:
int[] integers;

integers = new int[32];
string[] myArray = {"first element", "second element", "third element"};
Làm việc với mảng (Working with Arrays)


Ta có thể tìm được chiều dài của mảng sau nhờ vào thuộc tính Length thí dụ sau :
int arrayLength = integers.Length
Nếu các thành phần của mảng là kiểu định nghĩa trước (predefined types), ta có
thể sắp xếp tăng dần vào phương thức gọi là static Array.Sort() method:
Array.Sort(myArray);
Cuối cùng chúng ta có thể đảo ngược mảng đã có nhờ vào the static Reverse()
method:
Array.Reverse(myArray);
string[] artists = {"Leonardo", "Monet", "Van Gogh", "Klee"};
Array.Sort(artists);
Array.Reverse(artists);
foreach (string name in artists)
{
Console.WriteLine(name);
}
Mảng nhiều chiều (Multidimensional Arrays in C#)
Cú pháp :
type[,] array-name;
Thí dụ muốn khai báo một mảng hai chiều gồm hai hàng ba cột với phần tử kiểu
nguyên :
int[,] myRectArray = new int[2,3];
Bạn có thể khởi gán mảng xem các ví dụ sau về mảng nhiều chiều:
int[,] myRectArray = new int[,]{ {1,2},{3,4},{5,6},{7,8}}; // mảng 4 hàng 2 cột
string[,] beatleName = { {"Lennon","John"},
{"McCartney","Paul"},

{"Harrison","George"},
{"Starkey","Richard"} };


chúng ta có thể sử dụng :
string[,,] my3DArray;
double [, ] matrix = new double[10, 10];
for (int i = 0; i < 10; i++)
{
for (int j=0; j < 10; j++)
matrix[i, j] = 4;
}
Mảng jagged
Một loại thứ 2 của mảng nhiều chiều trong C# là Jagged array. Jagged là một
mảng mà mỗi phần tử là một mảng với kích thước khác nhau. Những mảng con
này phải đuợc khai báo từng mảng con một.
Thí dụ sau đây khai báo một mảng jagged hai chiều nghĩa là hai cặp [], gồm 3
hàng mỗi hàng là một mảng một chiều:
int[][] a = new int[3][];
a[0] = new int[4];
a[1] = new int[3];
a[2] = new int[1];
Khi dùng mảng jagged ta nên sử dụng phương thức GetLength() để xác định số
lượng cột của mảng. Thí dụ sau nói lên điều này:
using System;
namespace Wrox.ProCSharp.Basics
{
class MainEntryPoint
{
static void Main()

{
// Declare a two-dimension jagged array of authors' names
string[][] novelists = new string[3][];
novelists[0] = new string[] {
"Fyodor", "Mikhailovich", "Dostoyevsky"};
novelists[1] = new string[] {
"James", "Augustine", "Aloysius", "Joyce"};


novelists[2] = new string[] {
"Miguel", "de Cervantes", "Saavedra"};
// Loop through each novelist in the array
int i;
for (i = 0; i < novelists.GetLength(0); i++)
{
// Loop through each name for the novelist
int j;
for (j = 0; j < novelists[i].GetLength(0); j++)
{
// Display current part of name
Console.Write(novelists[i][j] + " ");
}
// Start a new line for the next novelist
Console.Write("\n");
}
}
}
}
Kết quả chương trình sau khi chạy:
csc AuthorNames.cs

Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.
AuthorNames
Fyodor Mikhailovich Dostoyevsky
James Augustine Aloysius Joyce
Miguel de Cervantes Saavedra
10.2. Danh sỏch
10.2.1. Khỏi nim danh sỏch tuyn tớnh
Danh sách là một tËp hỵp cã thø tù nh-ng bao gåm mét sè
biÕn động các phần tử (x1, x2, . . ., xn)
nếu n = 0 ta có một danh sách rỗng.
Một danh sách mà quan hệ lân cận đ-ợc hiển thị gọi
là danh sách tuyến tính (linear list).
VD: Véc tơ chính là một tr-ờng hợp đặc biệt của danh
sách tuyến tính xét tại một thời điểm nào đấy.


Danh sách tuyến tính là một danh sách hoặc rỗng
(không có phần tử nào) hoặc có dạng (a1, a2, . . ., an)
víi ai (1 ≤ i ≤ n) lµ các dữ liệu nguyên tử. Trong danh
sách tuyến tính luôn tồn tại một phần tử đầu a1, phần
tử cuối an. Đối với mỗi phần tử ai bất kỳ với 1 i n
- 1 thì có một phần tử ai+1 gọi là phần tử sau ai, và với
2 i n thì có một phần tử ai - 1 gọi là phần tử tr-ớc
ai. ai đ-ợc gọi là phần tử thứ i của danh sách tuyến
tính, n đ-ợc gọi là độ dài hoặc kích th-ớc của danh
sách.
Mỗi phần tử trong danh sách th-ờng là một bản ghi
( gồm một hoặc nhiều tr-ờng (fields)) đó là phần thông

tin nhỏ nhất có thể tham khảo. VD: Danh sách sinh viên
trong một lớp là một danh sách tuyến tính mà mỗi phần
tử ứng với một sinh viên, nó bao gồm các tr-ờng:
MÃ SV (STT), Họ và tên, Ngày sinh, Quê quán, . . .
10.2.2. Các phép toán thao tác trên danh sách:
+ Phép bổ sung một phần tử vào trong danh sách
(Insert) .
+ Phép loại bỏ một phần tử trong danh sách
(Delete).
+ Phép ghép nối 2 hoặc nhiều danh sách.
+ Phép tách một danh sách thành nhiều danh sách.
+ Phép sao chÐp mét danh s¸ch.
+ PhÐp cËp nhËt (update) danh s¸ch.
+ Phép sắp xếp các phần tử trong danh sách theo
thứ tự ấn định.
+ Phép tìm kiếm một phần tử trong danh sách theo
giá trị ấn định của một tr-ờng nào đó.
Trong đó phép bổ sung và phép loại bỏ là hai phép
toán th-ờng xuyên đ-ợc sử dụng trong danh sách.
Tệp cũng là một tr-ờng hợp của danh sách nó có
kích th-ớc lớn và th-ờng đ-ợc l-u trữ ở bộ nhớ ngoài.
Còn danh sách nói chung th-ờng đ-ợc xử lý ở bộ nhớ
trong.
Bộ nhớ trong đ-ợc hình dung nh- một dÃy các từ
máy(words) có thứ tự, mỗi từ máy ứng với một địa chỉ.
Mỗi từ máy chứa từ 8 ữ 64 bits, việc tham khảo đến nội
dung của nó thông qua địa chỉ.
+ Cách xác định địa chỉ của một phần tử trong danh
sách: Có 2 cách xác định địa chỉ:
Cách 1: Dựa vào đặc tả của dữ liệu cần tìm . Địa

chỉ này gọi là địa chỉ tính đ-ợc (hay địa chỉ trực
tiếp).
VD: Xác định địa chỉ của các phần tử trong véc tơ,
ma trận thông qua các chỉ số.


Cách 2: L-u trữ các địa chỉ cần thiết ở trong bộ
nhớ, khi cần xác định sẽ lấy ở đó ra. Địa chỉ này đ-ợc
gọi là con trỏ (pointer) hay mối nối (link).
L-u trữ kế tiếp của danh sách tuyến tính.
L-u trữ kế tiếp là phơng pháp l-u trữ sử dụng mảng
một chiều làm cấu trúc l-u trữ của danh sách tuyến tính
nghĩa là có thể dùng một véc tơ l-u trữ Vi với 1 i n
để l-u trữ một danh sách tuyến tính (a1, a2, . . ., an)
trong đó phần tử ai đ-ợc chứa ở Vi.
Ưu ®iĨm : Tèc ®é truy nhËp nhanh, dƠ thao t¸c
trong việc bổ sung, loại bỏ và tìm kiếm phần tử trong
danh sách.
Nh-ợc điểm: Do số phần tử trong danh sách tun
tÝnh th-êng biÕn ®éng (kÝch th-íc n thay ®ỉi) dÉn đến
hiện t-ợng lÃng phí bộ nhớ. Mặt khác nếu dự trữ đủ rồi
thị việc bổ sung hay loại bỏ một phần tử trong danh
sách mà không phải là phần tử cuối sẽ đòi hỏi phải dồn
hoặc dÃn danh sách (nghĩa là phải dịch chuyển một số
phần tử để lấy chỗ bổ sung hay tiến lên để lấp chỗ phần
tử bị lo¹i bá) sÏ tèn nhiỊu thêi gian.
Nhu cầu xây dựng cấu trúc dữ liệu động
Với các cấu trúc dữ liệu được xây dựng từ các kiểu cơ sở như: kiểu thực, kiểu
nguyên, kiểu ký tự ... hoặc từ các cấu trúc đơn giản như mẩu tin, tập hợp, mảng ...
lập trình viên có thể giải quyết hầu hết các bài toán đặt ra. Các đối tượng dữ liệu

được xác định thuộc những kiểu dữ liệu này có đặc điểm chung là khơng thay đổi
được kích thước, cấu trúc trong q trình sống, do vậy thường cứng ngắt, gị bó
khiến đơi khi khó diễn tả được thực tế vốn sinh động, phong phú. Các kiểu dữ liệu
kể trên được gọi là các kiểu dữ liệu tĩnh.
Ví dụ :
1.

Trong thực tế, một số đối tượng có thể được định nghĩa đệ qui, ví dụ để
mơ tả đối tượng "con người" cần thể hiện các thông tin tối thiểu như :
Họ tên
Số CMND
Thông tin về cha, mẹ
Ðể biễu diễn một đối tượng có nhiều thành phần thơng tin như trên có thể
sử dụng kiểu bản ghi. Tuy nhiên, cần lưu ý cha, mẹ của một người cũng là


các đối tượng kiểu NGƯỜI, do vậy về nguyên tắc cần phải có định nghĩa
như sau:
typedef

struct NGUOI{

char

Hoten[30];

int

So_CMND ;


NGUOI

Cha,Me;

};
Nhưng với khai báo trên, các ngơn ngữ lập trình gặp khó khăn trong việc
cài đặt không vượt qua được như xác định kích thước của đối tượng kiểu
NGUOI.
2.

Một số đối tượng dữ liệu trong chu kỳ sống của nó có thể thay đổi về
cấu trúc, độ lớn, như danh sách các học viên trong một lớp học có thể tăng
thêm, giảm đi ... Khi đó nếu cố tình dùng những cấu trúc dữ liệu tĩnh đã
biết như mảng để biểu diễn những đối tượng đó lập trình viên phải sử dụng
những thao tác phức tạp, kém tự nhiên khiến chương trình trở nên khó đọc,
do đó khó bảo trì và nhất là khó có thể sử dụng bộ nhớ một cách có hiệu
quả.

3.

Một lý do nữa làm cho các kiểu dữ liệu tĩnh không thể đáp ứng được nhu
cầu của thực tế là tổng kích thước vùng nhớ dành cho tất cả các biến tĩnh
có giới hạn. 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.

4.

Cuối cùng, do bản chất của các dữ liệu tĩnh, chúng sẽ chiếm vùng nhớ
đã dành cho chúng suốt quá trình hoạt động của chương trình. Tuy nhiên,
trong thực tế, có thể xảy ra trường hợp một dữ liệu nào đó chỉ tồn tại nhất

thời hay khơng thường xuyên trong quá trình hoạt động của chương trình.
Vì vậy việc dùng các CTDL tĩnh sẽ không cho phép sử dụng hiệu quả bộ
nhớ.

Do vậy, 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, 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 mới 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. Bài sau sẽ giới thiệu về các cấu trúc dữ liệu động và tập trung
khảo sát cấu trúc đơn giản nhất thuộc loại này là danh sách liên kết.


Bài 15: Thực hành cài đặt danh sách Stack và Queue
Bài 1
Hãy viết một chương trình hồn chỉnh có menu cho người sử dụng để minh họa
các thuật toán sau:
1. Khởi tạo Stack
2. Thêm các phần tử vào Stack
3. Lấy các phần tử ra khỏi Stack
4. Thoát
Bài 2.
Hãy viết một chương trình hồn chỉnh có menu cho người sử dụng để minh họa
các thuật toán sau:
1. Khởi tạo Queue
2. Thêm các phần tử vào Queue
3. Lấy các phần tử ra khỏi Queue
4. Thốt
Bài 3.
Dùng Stack để viết chương trình chuyển đổi một số từ hệ cơ số 10 sang hệ
cơ số nguyên a (0

Bài 4.
Mô phỏng việc tạo buffer in một file ra máy in.
Buffer xem như là một hàng, khi ra lệnh in file máy tính sẽ thực hiện một cách lặp
q trình sau cho đến hết file:
¾ Ðưa nội dung của tập tin vào buffer cho đến khi buffer đầy hoặc hết file.
¾ In nội dung trong buffer ra máy in cho tới khi hàng rỗng.
Hãy mô phỏng quá trình trên để in một file văn bản lên từng trang màn hình.


Bài 16: Danh sách nối kép (Double Linked List)
16.1. Định nghĩa và nguyên tắc tổ chức DS nối kép
Một số ứng dụng đòi hỏi chúng ta phải duyệt danh sách theo cả hai chiều một
cách hiệu quả. Chẳng hạn cho phần tử X cần biết ngay phần tử trước X và sau X
một cách mau chóng. Trong trường hợp này ta phải dùng hai con trỏ, một con trỏ
chỉ đến phần tử đứng sau (next), một con trỏ chỉ đến phần tử đứng trước
(previous). Với cách tổ chức này ta có một danh sách liên kết kép. Dạng của một
danh sách liên kép như sau:

Hình 16.1 Hình ảnh một danh sách liên kết kép
Các khai báo cần thiết
typedef ... ElementType; //kiểu nội dung của các phần tử trong danh sách
typedef struct Node{ ElementType Element; //lưu trữ nội dung phần tử //Hai con
trỏ trỏ tới phần tử trước và sau Node* Prev; Node* Next; };
typedef Node* Position; typedef Position DoubleList; Để quản lí một danh
sách liên kết kép ta có thể dùng một con trỏ trỏ đến một ô bất kỳ trong cấu trúc.
Hoàn toàn tương tự như trong danh sách liên kết đơn đã trình bày trong phần
trước, con trỏ để quản lí danh sách liên kết kép có thể là một con trỏ có kiểu giống
như kiểu phần tử trong danh sách và nó có thể được cấp phát ô nhớ (tương tự như
Header trong danh sách liên kết đơn) hoặc không được cấp phát ô nhớ. Ta cũng có
thể xem danh sách liên kết kép như là danh sách liên kết đơn, với một bổ sung duy

nhất là có con trỏ previous để nối kết các ơ theo chiều ngược lại. Theo quan điểm
này thì chúng ta có thể cài đặt các phép toán thêm (insert), xoá (delete) một phần
tử hoàn toàn tương tự như trong danh sách liên kết đơn và con trỏ Header cũng
cần thiết như trong danh sách liên kết đơn, vì nó chính là vị trí của phần tử đầu
tiên trong danh sách. Tuy nhiên, nếu tận dụng khả năng duyệt theo cả hai chiều thì
ta khơng cần phải cấp phát bộ nhớ cho Header và vị trí (position) của một phần tử
trong danh sách có thể định nghĩa như sau: Vị trí của phần tử ai là con trỏ trỏ tới ô
chứa ai, tức là địa chỉ ơ nhớ chứa ai. Nói nơm na, vị trí của ai là ơ chứa ai. Theo
định nghĩa vị trí như vậy các phép tốn trên danh sách liên kết kép sẽ được cài đặt
hơi khác với danh sách liên đơn. Trong cách cài đặt này, chúng ta không sử dụng ô
đầu mục như danh sách liên kết đơn mà sẽ quản lý danh sách một các trực tiếp
(header chỉ ngay đến ô đầu tiên trong danh sách).


16.2. Các phép toán thao tác trên danh sách nối kép
Tạo danh sách liên kết kép rỗng
Giả sử DL là con trỏ quản lí danh sách liên kết kép thì khi khởi tạo danh sách rỗng
ta cho con trỏ này trỏ NULL (không cấp phát ô nhớ cho DL), tức là gán
DL=NULL.
void MakeNull_List (DoubleList *DL){
(*DL)= NULL;
}
Kiểm tra danh sách liên kết kép rỗng
Rõ ràng, danh sách liên kết kép rỗng khi và chỉ khi chỉ điểm đầu danh sách
không trỏ tới một ơ xác định nào cả. Do đó ta sẽ kiểm tra DL = NULL.
int Empty (DoubleList DL){
return (DL==NULL);
}
Xóa một phần tử ra khỏi danh sách liên kết kép
Để xố một phần tử tại vị trí p trong danh sách liên kết kép được trỏ bởi DL, ta

phải chú ý đến các trường hợp sau:
- Danh sách rỗng, tức là DL=NULL: chương trình con dừng.
- Trường hợp danh sách khác rỗng, tức là DL!=NULL, ta phải phân biệt hai
trường hợp
Ơ bị xố khơng phải là ơ được trỏ bởi DL, ta chỉ cần cập nhật lại các con
trỏ để nối kết ô trước p với ô sau p, các thao tác cần thiết là (xem hình II.16):
Nếu (p->previous!=NULL) thì p->previous->next=p->next;
Nếu (p->next!=NULL) thì p->next->previous=p->previous;
Xố ơ đang được trỏ bởi DL, tức là p=DL: ngoài việc cập nhật lại các con
trỏ để nối kết các ô trước và sau p ta cịn phải cập nhật lại DL, ta có thể cho DL trỏ
đến phần tử trước nó (DL = p->Previous) hoặc đến phần tử sau nó (DL = p->Next)
tuỳ theo phần tử nào có mặt trong danh sách. Đặc biệt, nếu danh sách chỉ có một
phần tử tức là p->Next=NULL và p->Previous=NULL thì DL=NULL.


Hình 16.2 Xóa phần tử tại vị trí p
void Delete_List (Position p, DoubleList *DL){
if (*DL == NULL) printf(”Danh sach rong”);
else{
if (p==*DL) (*DL)=(*DL)->Next;
//Xóa phần tử đầu tiên của danh sách nên phải thay đổi
DL
else p->Previous->Next=p->Next;
if (p->Next!=NULL)
p->Next->Previous=p->Previous;
free(p);
}
}
Thêm phần tử vào danh sách liên kết kép
Để thêm một phần tử x vào vị trí p trong danh sách liên kết kép được trỏ bởi

DL, ta cũng cần phân biệt mấy trường hợp sau:
Danh sách rỗng, tức là DL = NULL: trong trường hợp này ta không quan tâm
đến giá trị của p. Để thêm một phần tử, ta chỉ cần cấp phát ô nhớ cho nó, gán giá
trị x vào trường Element của ô nhớ này và cho hai con trỏ previous, next trỏ tới
NULL cịn DL trỏ vào ơ nhớ này, các thao tác trên có thể viết như sau:
DL=(Node*)malloc(sizeof(Node));
DL->Element = x;
DL->Previous=NULL;
DL->Next =NULL;
Nếu DL!=NULL, sau khi thêm phần tử x vào vị trí p ta có kết quả như hình 16.3


Hình 16.3: Danh sách trước khi thêm phần tử x

Hình 16.4: Danh sách sau khi thêm phần tử x vào tại vị trí p
(phần tử tại vị trí p cũ trở thành phần tử "sau" của x)

Lưu ý: các kí hiệu p, p->Next, p->Previous trong hình 16.4 để chỉ các ô trước
khi thêm phần tử x, tức là nó chỉ các ơ trong hình 16.3.
Trong trường hợp p=DL, ta có thể cập nhật lại DL để DL trỏ tới ô mới thêm vào
hoặc để nó trỏ đến ơ tại vị trí p cũ như nó đang trỏ cũng chỉ là sự lựa chọn trong
chi tiết cài đặt.
void
Insert_List
(ElementType
X,Position
p,
DoubleList *DL){
if (*DL == NULL){
(*DL)=(Node*)malloc(sizeof(Node));

(*DL)->Element = X;
(*DL)->Previous =NULL;
(*DL)->Next =NULL;
}
else{
Position temp;
temp=(Node*)malloc(sizeof(Node));
temp->Element=X;


temp->Next=p;
temp->Previous=p->Previous;
if (p->Previous!=NULL)
p->Previous->Next=temp;
p->Previous=temp;
}
}


Bài 17: Thực hành cài đặt DANH SÁCH LIÊN KẾT KÉP
Viết chương trình lưu trữ một danh sách các số nguyên, sắp xếp danh sách
theo thứ tự (tăng hoặc giảm), trộn 2 danh sách có thứ tự để được một danh sách mới
có thứ tự.
Yêu cầu chi tiết:
1. Viết các khai báo cần thiết để cài đặt một danh sách các số nguyên.
2. Viết thủ tục khởi tạo một danh sách rỗng.
3. Viết hàm kiểm tra danh sách rỗng.
4. Viết thủ tục nhập một danh sách.
5. Viết thủ tục hiển thị danh sách ra màn hình.
6. Viết thủ tục sắp xếp danh sách theo thứ tự (tăng hoặc giảm).

7. Xen một phần tử mới x vào danh sách sau cho danh sách mới vẫn bảo đảm thứ
tự.
8. Xóa một phần tử x ra khỏi danh sách sao cho danh sách mới vẫn bảo đảm thứ
tự.
9. viết thủ tục trộn 2 danh sách đã có thứ tự thành một danh sách mới sao cho danh
sách mới vẫn bảo đảm thứ tự.
Viết chương trình nhập vào một danh sách các số nguyên và thực hiện các
yêu cầu sau:
• Hiển thị danh sách vừa nhập.
• Sắp xếp danh sách theo thứ tự. Hiển thị danh sách sau khi sắp xếp.
• Xen một phần tử mới vào danh sách. Hiển thị danh sách mới sau khi xen.
• Xóa một phần tử khỏi danh sách. Hiển thị danh sách mới sau khi xóa.
• Nhập 2 danh sách, sắp xếp 2 danh sách theo thứ tự, sau đó trộn 2 danh sách
này để được một danh sách mới cũng có thứ tự. Hiển thị danh sách mới ra
màn hình để kiểm tra.


Bài 18: Kiểu dữ liệu trừu tượng tập hợp và hàm băm
18.1. Tập hợp
18.1.1. Khái niệm
Tập hợp là một khái niệm cơ bản trong toán học. Tập hợp được dùng để mơ
hình hố hay biểu diễn một nhóm bất kỳ các đối tượng trong thế giới thực cho nên
nó đóng vai trị rất quan trọng trong mơ hình hố cũng như trong thiết kế các giải
thuật.
Khái niệm tập hợp cũng như trong tốn học, đó là sự tập hợp các thành viên
(members) hoặc phần tử (elements). Tất cả các phần tử của tập hợp là khác nhau.
Tập hợp có thể có thứ tự hoặc khơng có thứ tự, tức là, có thể có quan hệ thứ tự xác
định trên các phần tử của tập hợp hoặc không. Tuy nhiên, trong chương này,
chúng ta giả sử rằng các phần tử của tập hợp có thứ tự tuyến tính, tức là, trên tập
hợp S có quan hệ < và = thoả mản hai tính chất:

Với mọi a,b ≍ S thì aVới mọi a,b,c ≍ S, a18.1.2. Cài đặt các phép tốn thao tác trên cấu trúc tập hợp
Cũng như các kiểu dữ liệu trừu tượng khác, các phép tốn kết hợp với mơ hình
tập hợp sẽ tạo thành một kiểu dữ liệu trừu tượng là rất đa dạng. Tùy theo nhu cầu
của các ứng dụng mà các phép toán khác nhau sẽ được định nghĩa trên tập hợp. Ở
đây ta đề cập đến một số phép tốn thường gặp nhất như sau
• Thủ tục UNION(A,B,C) nhận vào 3 tham số là A,B,C; Thực hiện phép toán
lấy hợp của hai tập A và B và trả ra kết quả là tập hợp C = A ≯ B.
• Thủ tục INTERSECTION(A,B,C) nhận vào 3 tham số là A,B,C; Thực hiện
phép toán lấy giao của hai tập A và B và trả ra kết quả là tập hợp C = A ∩ B.
• Thủ tục DIFFERENCE(A,B,C) nhận vào 3 tham số là A,B,C; Thực hiện phép
toán lấy hợp của hai tập A và B và trả ra kết quả là tập hợp C = A\B
• Hàm MEMBER(x,A) cho kết quả kiểu logic (đúng/sai) tùy theo x có thuộc A
hay khơng. Nếu x ≍ A thì hàm cho kết quả là 1 (đúng), ngược lại cho kết quả
0 (sai).
• Thủ tục MAKENULLSET(A) tạo tập hợp A tập rỗng
• Thủ tục INSERTSET(x,A) thêm x vào tập hợp A
• Thủ tục DELETESET(x,A) xoá x khỏi tập hợp A


• Thủ tục ASSIGN(A,B) gán A cho B ( tức là B:=A )
• Hàm MIN(A) cho phần tử bé nhất trong tập A
• Hàm EQUAL(A,B) cho kết quả TRUE nếu A=B ngược lại cho kết quả FALSE
1. Cài đặt tập hợp bằng vector Bit
Hiệu quả của một cách cài đặt tập hợp cụ thể phụ thuộc vào các phép toán và kích
thước tập hợp. Hiệu quả này cũng sẽ phụ thuộc vào tần suất sử dụng các phép toán
trên tập hợp. Chẳng hạn nếu chúng ta thường xuyên sử dụng phép thêm vào và
loại bỏ các phần tử trong tập hợp thì chúng ta sẽ tìm cách cài đặt hiệu quả cho các
phép tốn này. Cịn nếu phép tìm kiếm một phần tử xảy ra thường xun thì ta có

thể phải tìm cách cài đặt phù hợp để có hiệu quả tốt nhất. Ở đây ta xét một trường
hợp đơn giản là khi toàn thể tập hợp của chúng ta là tập hợp con của một tập hợp
các số nguyên nằm trong phạm vi nhỏ từ 1.. n chẳng hạn thì chúng ta có thể dùng
một mảng kiểu Boolean có n phần tử để cài đặt tập hợp (ta gọi là vectơ bít), bằng
cách cho phần tử thứ i của mảng này giá trị TRUE nếu i thuộc tập hợp hoặc cho
mảng lưu kiểu 0-1. Nếu nội dung phần tử trong mảng tại vị trí i là 1 nghĩa là i tồn
tại trong tập hợp và ngược lại, nội dung là 0 nghĩa là phần tử i đó khơng tồn tại
trong tập hợp. Ví dụ: Giả sử các phần tử của tập hợp được lấy trong các số nguyên
từ 1 đến 10 thì mỗi tập hợp được biểu diễn bởi một mảng một chiều có 10 phần tử
với các giá trị phần tử thuộc kiểu logic. Chẳng hạn tập hợp A={1,3,5,8} được biểu

diễn trong mảng có 10 phần tử như sau:
Cách biểu diễn này chỉ thích hợp trong điều kiện là mọi thành viên của tất cả
các tập hợp đang xét phải có giá trị nguyên hoặc có thể đặt tương ứng duy nhất với
số nguyên nằm trong một phạm vi nhỏ. Có thể dễ dàng nhận thấy khai báo cài đặt
như sau
const maxlength = 100;
// giá trị phần tử lớn nhất trong tập hợp số nguyên
không âm
typedef int SET [maxlength];
Tạo một tập hợp rỗng
Để tạo một tập hợp rỗng ta cần đặt tất cả các nội dung trong tập hợp từ vị trí 0
đến vị trí maxlength đều bằng 0. Câu lệnh được viết như sau :
void makenull(SET a)