Với các sinh viên chuyên nghành tin học thì cụm từ Cấu trúc dữ liệu (Data Structure) không còn là xa lạ. Đây là một môn học bắt buộc và sẽ là thực sự khó cho bất kỳ sinh viên nào nếu không có sự chuẩn bị kỹ lưỡng và dành cách tiếp cận tích cực cho môn học này. Vậy Cấu trúc dữ liệu là gì?Dưới đây là danh sách các bài hướng dẫn trong loạt bài Cấu trúc dữ liệu và Giải thuật.
Trang 1Cấu trúc dữ liệu (Data Structure) là gì ?
Cấu trúc dữ liệu là cách lưu trữ, tổ chức dữ liệu có thứ tự, có hệ thống để dữ liệu có thể được sử dụng một cách hiệu quả.
Dưới đây là hai khái niệm nền tảng hình thành nên một cấu trúc dữ liệu:
• Interface: Mỗi cấu trúc dữ liệu có một Interface Interface biểu diễn một tập hợp các phép
tính mà một cấu trúc dữ liệu hỗ trợ Một Interface chỉ cung cấp danh sách các phép tính được hỗ trợ, các loại tham số mà chúng có thể chấp nhận và kiểu trả về của các phép tính này
• Implementation (có thể hiểu là sự triển khai): Cung cấp sự biểu diễn nội bộ của một cấu
trúc dữ liệu Implementation cũng cung cấp phần định nghĩa của giải thuật được sử dụng trong các phép tính của cấu trúc dữ liệu
Đặc điểm của một Cấu trúc dữ liệu
• Chính xác: Sự triển khai của Cấu trúc dữ liệu nên triển khai Interface của nó một cách chính
xác
• Độ phức tạp về thời gian (Time Complexity): Thời gian chạy hoặc thời gian thực thi của
các phép tính của cấu trúc dữ liệu phải là nhỏ nhất có thể
• Độ phức tạp về bộ nhớ (Space Complexity): Sự sử dụng bộ nhớ của mỗi phép tính của
cấu trúc dữ liệu nên là nhỏ nhất có thể
Tại sao Cấu trúc dữ liệu là cần thiết ?
Ngày nay, các ứng dụng ngày càng phức tạp và lượng dữ liệu ngày càng lớn với nhiều kiểu đa dạng Việc này làm xuất hiện 3 vấn đề lớn mà mỗi lập trình viên phải đối mặt:
• Tìm kiếm dữ liệu: Giả sử có 1 triệu hàng hóa được lưu giữ vào trong kho hàng hóa Và giả
sử có một ứng dụng cần để tìm kiếm một hàng hóa Thì mỗi khi thực hiện tìm kiếm, ứng dụng này
sẽ phải tìm kiếm 1 hàng hóa trong 1 triệu hàng hóa Khi dữ liệu tăng lên thì việc tìm kiếm sẽ càng trở lên chậm và tốn kém hơn
• Tốc độ bộ vi xử lý: Mặc dù bộ vi xử lý có tốc độ rất cao, tuy nhiên nó cũng có giới hạn và
khi lượng dữ liệu lên tới hàng tỉ bản ghi thì tốc độ xử lý cũng sẽ không còn được nhanh nữa
Trang 2• Đa yêu cầu: Khi hàng nghìn người dùng cùng thực hiện một phép tính tìm kiếm trên một
Web Server thì cho dù Web Server đó có nhanh đến mấy thì việc phải xử lý hàng nghìn phép tính cùng một lúc là thực sự rất khó
Để xử lý các vấn đề trên, các cấu trúc dữ liệu là một giải pháp tuyệt vời Dữ liệu có thể được tổ chức trong cấu trúc dữ liệu theo một cách để khi thực hiện tìm kiếm một phần tử nào đó thì dữ liệu yêu cầu sẽ được tìm thấy ngay lập tức
Độ phức tạp thời gian thực thi trong cấu trúc dữ liệu và giải thuật
Có 3 trường hợp thường được sử dụng để so sánh thời gian thực thi của các cấu trúc dữ liệu khác nhau:
• Trường hợp xấu nhất (Worst Case): là tình huống mà một phép tính của cấu trúc dữ liệu
nào đó tốn thời gian tối đa (thời gian dài nhất) Ví dụ với ba số 1, 2, 3 thì nếu sắp xếp theo thứ tự giảm dần thì thời gian thực thi sẽ là dài nhất (và đây là trường hợp xấu nhất); còn nếu sắp xếp theo thứ tự tăng dần thì thời gian thực thi sẽ là ngắn nhất (và đây là trường hợp tốt nhất)
• Trường hợp trung bình (Average Case): miêu tả thời gian thực thi trung bình một phép
tính của một cấu trúc dữ liệu
• Trường hợp tốt nhất (Best Case): là tình huống mà thời gian thực thi một phép tính của
một cấu trúc dữ liệu là ít nhất Ví dụ như trên
Thuật ngữ cơ bản trong Cấu trúc dữ liệu
• Dữ liệu: Dữ liệu là các giá trị hoặc là tập hợp các giá trị.
• Phần tử dữ liệu: Phần tử dữ liệu là một đơn vị đơn lẻ của giá trị.
• Các phần tử nhóm: Phần tử dữ liệu mà được chia thành các phần tử con thì được gọi là
các phần tử nhóm
• Các phần tử cơ bản: Phần tử dữ liệu mà không thể bị chia nhỏ thành các phần tử con thì
gọi là các phần tử cơ bản
• Thuộc tính và Thực thể: Một thực thể là cái mà chứa một vài thuộc tính nào đó, và các
thuộc tính này có thể được gán các giá trị
Trang 3• Tập hợp thực thể: Các thực thể mà có các thuộc tính tương tự nhau thì cấu thành một tập
hợp thực thể
• Trường: Trường là một đơn vị thông tin cơ bản biểu diễn một thuộc tính của một thực thể.
• Bản ghi: Bản ghi là một tập hợp các giá trị trường của một thực thể đã cho.
• File: Là một tập hợp các bản ghi của các thực thể trong một tập hợp thực thể đã cho.
Cài đặt IDE để biên dịch và thực thi C
Có một số IDE có sẵn và miễn phí để biên dịch và thực thi các chương trình C Bạn có thể
chọn Dev-C++, Code:: Blocks, hoặc Turbo C Tuy nhiên, lựa chọn phổ biến nhất và hay được sử
dụng nhất là Dev-C++ và các chương trình C trong loạt bài này cũng được biên dịch và thực thi trong Dev-C++
Bạn truy cập theo link sau để tải Dev-C++: Tải Dev-C++ Trên trang này cũng bao gồm cả Code:: Blocks Sau khi bạn tải xong, để cài đặt IDE này, bạn chỉ cần vào Google và gõ "cài đặt dev-c++" là
có rất nhiều video hướng dẫn chi tiết, cho nên mình không cần trình bày thêm nữa
Sau khi đã cài đặt xong, để biên dịch và thực thi một chương trình C, bạn: (a) vào File -> New ->
Project -> Console Application -> C project, sau đó nhập tên vào hoặc (b) File -> New -> Source File Cuối cùng, sao chép và dán chương trình C vào file bạn vừa tạo Để biên dịch và thực thi,
chọn Execute -> Compile & Run.
Cài đặt để chạy trên Command Prompt
Nếu bạn muốn cài đặt để biên dịch và chạy trên Command Prompt, thì bạn nên đọc phần sau đây
Nếu bạn đang muốn cài đặt chương trình C, bạn cần phải sử dụng 2 phần mềm trên máy tính của bạn: (a) Chương trình soạn văn bản - Text Editor và (b) Bộ biên dịch C
Text Editor
Được sử dụng để soạn thảo các chương trình Ví dụ về một vài trình editor như Window Notepad, Notepad ++, vim hay vi…
Tên và các phiên bản của các trình editor có thể thay đổi theo các hệ điều hành Ví dụ, Notepad được sử dụng trên Windows, hoặc vim hay vi được sử dụng trên Linux hoặc UNIX
Trang 4Các file bạn tạo trong trình editor được gọi là source file (file nguồn) và chứa các chương trình
code Các file trong chương trình C thường được đặt tên với phần mở rộng ".c".
Trước khi bắt đầu chương trình của bạn, hãy chắc chắn bạn có một trình editor trên máy tính và bạn có đủ kinh nghiệm để viết các chương trình máy tính, lưu trữ trong file và thực thi nó
Bộ biên dịch C
Mã nguồn được viết trong file nguồn dưới dạng có thể đọc được Nó sẽ được biên dịch thành mã máy, để cho CPU có thể thực hiện các chương trình này dựa trên các lệnh được viết
Bộ biên dịch được sử dụng để biên dịch mã nguồn (source code) của bạn đến chương trình có thể thực thi Tôi giả sử bạn có kiến thức cơ bản về một bộ biên dịch ngôn ngữ lập trình
Bộ biên dịch thông dụng nhất là bộ biên dịch GNU C/C++, mặt khác bạn có thể có các bộ biên dịch khác như HP hoặc Solaris với Hệ điều hành tương ứng
Dưới đây là phần hướng dẫn giúp bạn cách cài đặt bộ biên dich GNU C/C++ trên các hệ điều hành khác nhau Tôi đang đề cập đến C/C++ bởi vì bộ biên dịch GNU gcc hoạt động cho cả ngôn ngữ C
và C++
Cài đặt trên môi trường UNIX/Linux
Nếu bạn đang sử dụng Linux hoặc UNIX, bạn có thể kiểm tra bộ GCC đã được cài đặt trên môi
trường của bạn chưa bằng lệnh sau đây:
$ gcc - v
Nếu bạn có bộ cài đặt GNU trên máy tính của bạn, sau đó nó sẽ phản hồi một thông báo sau:
Using built - in specs
Target : i386 - redhat - linux
Configured with : / configure prefix =/ usr .
Thread model : posix
gcc version 4.1 2 20080704 ( Red Hat 4.1 2 - 46 )
Nếu bộ GCC chưa được cài đặt, bạn có thể cài đặt nó với hướng dẫn trên đường link dưới đây: http://gcc.gnu.org/install/
Bài hướng dẫn này được viết dựa trên Linux và tất cả các ví dụ dược biên dịch trên Cent OS của
hệ thống Linux
Trang 5Cài đặt trên môi trường Mac OS
Nếu bạn sử dụng hệ điều hành Mac OS X, cách đơn giản nhất để có GCC là download môi trường phát triển Xcode, bạn có thể sử dụng bộ biên dịch GNU cho C/C++
Xcode được sẵn dưới link sau: developer.apple.com/technologies/tools/
Cài đặt trên Windows
Để cài đặt GCC trên Windows bạn cần phải cài đặt MinGW Để cài đặt MinGW, bạn truy cập vào www.mingw.org, và theo hướng dẫn trên trang download này Download phiên bản mới nhất cho chương trình MinGW, dưới tên MinGW-<version>.exe
Khi cài đặt MinWG, ít nhất bạn phải cài đặt gcc-core, gcc-g++, binutils và MinGW runtime, nhưng bạn có thể cài đặt nhiều hơn
Thêm thư mục con bin trong nơi cài đặt MinGW vào biến môi trường PATH của bạn, bạn có thể sử
dụng trực tiếp các công cụ dưới dạng command line một các dễ dàng
Khi quá trình cài đặt hoàn tất, bạn có thể chạy gcc, g++, ar, ranlib, dlltool và các công cụ GNU khác trên Windows command line
Phân tích tiệm cận là gì ?
Phân tích tiệm cận của một giải thuật là khái niệm giúp chúng ta ước lượng được thời gian chạy (Running Time) của một giải thuật Sử dụng phân tích tiệm cận, chúng ta có thể đưa ra kết luận tốt nhất về các tình huống trường hợp tốt nhất, trường hợp trung bình, trường hợp xấu nhất của một giải thuật Để tham khảo về các trường hợp này, bạn có thể tìm hiểu chương Cấu trúc dữ liệu là gì
?
Phân tích tiệm cận tức là tiệm cận dữ liệu đầu vào (Input), tức là nếu giải thuật không có Input thì kết luận cuỗi cùng sẽ là giải thuật sẽ chạy trong một lượng thời gian cụ thể và là hằng số Ngoài nhân tố Input, các nhân tố khác được xem như là không đổi
Phân tích tiệm cận nói đến việc ước lượng thời gian chạy của bất kỳ phép tính nào trong các bước
tính toán Ví dụ, thời gian chạy của một phép tính nào đó được ước lượng là một hàm f(n) và với một phép tính khác là hàm g(n 2 ) Điều này có nghĩa là thời gian chạy của phép tính đầu tiên sẽ tăng
tuyến tính với sự tăng lên của n và thời gian chạy của phép tính thứ hai sẽ tăng theo hàm mũ khi n tăng lên Tương tự, khi n là khá nhỏ thì thời gian chạy của hai phép tính là gần như nhau
Thường thì thời gian cần thiết bởi một giải thuật được chia thành 3 loại:
Trang 6• Trường hợp tốt nhất: là thời gian nhỏ nhất cần thiết để thực thi chương trình.
• Trường hợp trung bình: là thời gian trung bình cần thiết để thực thi chương trình.
• Trường hợp xấu nhất: là thời gian tối đa cần thiết để thực thi chương trình.
Asymptotic Notation trong Cấu trúc dữ liệu và giải thuật
Dưới đây là các Asymptotic Notation được sử dụng phổ biến trong việc ước lượng độ phức tạp thời gian chạy của một giải thuật:
• Ο Notation
• Ω Notation
• θ Notation
Big Oh Notation, Ο trong Cấu trúc dữ liệu và giải thuật
Ο(n) là một cách để biểu diễn tiệm cận trên của thời gian chạy của một thuật toán Nó ước lượng
độ phức tạp thời gian trường hợp xấu nhất hay chính là lượng thời gian dài nhất cần thiết bởi một giải thuật (thực thi từ bắt đầu cho đến khi kết thúc) Đồ thị biểu diễn như sau:
Ví dụ, gọi f(n) và g(n) là các hàm không giảm định nghĩa trên các số nguyên dương (tất cả các hàm thời gian đều thỏa mãn các điều kiện này):
Ο(f n )) = { g( n ) : n ế u t ồ n t ạ i c > 0 v à n 0 sao cho g( n ) ≤ c f n ) v ớ i m ọ i n > n 0 }
Trang 7Omega Notation, Ω trong Cấu trúc dữ liệu và giải thuật
The Ω(n) là một cách để biểu diễn tiệm cận dưới của thời gian chạy của một giải thuật Nó ước lượng độ phức tạp thời gian trường hợp tốt nhất hay chính là lượng thời gian ngắn nhất cần thiết bởi một giải thuật Đồ thị biểu diễn như sau:
Ví dụ, với một hàm f(n):
Ω(f n )) ≥ { g( n ) : n ế u t ồ n t ạ i c > 0 v à n 0 sao cho g( n ) ≤ c f n ) v ớ i m ọ i n > n 0 }
Theta Notation, θ trong Cấu trúc dữ liệu và giải thuật
The θ(n) là cách để biểu diễn cả tiệm cận trên và tiệm cận dưới của thời gian chạy của một giải thuật Bạn nhìn vào đồ thì sau:
θ(f n )) = { g( n ) n ế u v à ch ỉ n ếu g( n ) = Ο(f n )) v à g( n ) = Ω(f n )) v ớ i m ọ i n > n 0 }
Một số Asymptotic Notation phổ biến trong cấu trúc dữ liệu và giải thuật
Trang 8logarit − Ο(log n)
Giải thuật tham lam (Greedy Algorithm)
Trang trước
Trang sau
Giải thuật tham lam là gì ?
Tham lam (hay tham ăn) là một trong những phương pháp phổ biến nhất để thiết kế giải thuật Nếu bạn đã đọc truyện dân gian thì sẽ có câu chuyện như thế này: trên một mâm cỗ có nhiều món ăn,
Trang 9món nào ngon nhất ta sẽ ăn trước, ăn hết món đó ta sẽ chuyển sang món ngon thứ hai, và chuyển tiếp sang món thứ ba, …
Rất nhiều giải thuật nổi tiếng được thiết kế dựa trên ý tưởng tham lam, ví dụ như giải thuật cây khung nhỏ nhất của Dijkstra, giải thuật cây khung nhỏ nhất của Kruskal, …
Giải thuật tham lam (Greedy Algorithm) là giải thuật tối ưu hóa tổ hợp Giải thuật tìm kiếm, lựa
chọn giải pháp tối ưu địa phương ở mỗi bước với hi vọng tìm được giải pháp tối ưu toàn cục
Giải thuật tham lam lựa chọn giải pháp nào được cho là tốt nhất ở thời điểm hiện tại và sau đó giải bài toán con nảy sinh từ việc thực hiện lựa chọn đó Lựa chọn của giải thuật tham lam có thể phụ thuộc vào lựa chọn trước đó Việc quyết định sớm và thay đổi hướng đi của giải thuật cùng với việc không bao giờ xét lại các quyết định cũ sẽ dẫn đến kết quả là giải thuật này không tối ưu để tìm giải pháp toàn cục
Bạn theo dõi một bài toán đơn giản dưới đây để thấy cách thực hiện giải thuật tham lam và vì sao lại có thể nói rằng giải thuật này là không tối ưu
Bài toán đếm số đồng tiền
Yêu cầu là hãy lựa chọn số lượng đồng tiền nhỏ nhất có thể sao cho tổng mệnh giá của các đồng tiền này bằng với một lượng tiền cho trước
Nếu tiền đồng có các mệnh giá lần lượt là 1, 2, 5, và 10 xu và lượng tiền cho trước là 18 xu thì giải thuật tham lam thực hiện như sau:
• Bước 1: Chọn đồng 10 xu, do đó sẽ còn 18 – 10 = 8 xu.
• Bước 2: Chọn đồng 5 xu, do đó sẽ còn là 3 xu.
• Bước 3: Chọn đồng 2 xu, còn lại là 1 xu.
• Bước 4: Cuối cùng chọn đồng 1 xu và giải xong bài toán.
Bạn thấy rằng cách làm trên là khá ổn, và số lượng đồng tiền cần phải lựa chọn là 4 đồng tiền Nhưng nếu chúng ta thay đổi bài toán trên một chút thì cũng hướng tiếp cận như trên có thể sẽ không đem lại cùng kết quả tối ưu
Chẳng hạn, một hệ thống tiền tệ khác có các đồng tiền có mệnh giá lần lượt là 1, 7 và 10 xu và lượng tiền cho trước ở đây thay đổi thành 15 xu thì theo giải thuật tham lam thì số đồng tiền cần
Trang 10chọn sẽ nhiều hơn 4 Với giải thuật tham lam thì: 10 + 1 + 1 +1 + 1 + 1, vậy tổng cộng là 6 đồng tiền Trong khi cùng bài toán như trên có thể được xử lý bằng việc chỉ chọn 3 đồng tiền (7 + 7 +1)
Do đó chúng ta có thể kết luận rằng, giải thuật tham lam tìm kiếm giải pháp tôi ưu ở mỗi bước nhưng lại có thể thất bại trong việc tìm ra giải pháp tối ưu toàn cục
Ví dụ áp dụng giải thuật tham lam
Có khá nhiều giải thuật nổi tiếng được thiết kế dựa trên tư tưởng của giải thuật tham lam Dưới đây
là một trong số các giải thuật này:
• Bài toán hành trình người bán hàng
• Giải thuật cây khung nhỏ nhất của Prim
• Giải thuật cây khung nhỏ nhất của Kruskal
• Giải thuật cây khung nhỏ nhất của Dijkstra
• Bài toán xếp lịch công việc
• Bài toán xếp ba lô
Giải thuật chia để trị (divide and conquer)
Trang trước
Trang sau
Giải thuật chia để trị (Divide and Conquer)là gì ?
Phương pháp chia để trị (Divide and Conquer) là một phương pháp quan trọng trong việc thiết kế các giải thuật Ý tưởng của phương pháp này khá đơn giản và rất dễ hiểu: Khi cần giải quyết một bài toán, ta sẽ tiến hành chia bài toán đó thành các bài toán con nhỏ hơn Tiếp tục chia cho đến khi các bài toán nhỏ này không thể chia thêm nữa, khi đó ta sẽ giải quyết các bài toán nhỏ nhất này và cuối cùng kết hợp giải pháp của tất cả các bài toán nhỏ để tìm ra giải pháp của bài toán ban đầu