Mục tiêu của học phần: - Kiến thức: Phân biệt các loại cấu trúc dữ liệu cơ bản; Trình bày cấu trúc dữ liệu của cấu trúc cây và một số thao tác trên cây; Ước lượng thời gian thực
GIỚI THIỆU
Bài viết này nhấn mạnh tầm quan trọng của cấu trúc dữ liệu và giải thuật, đặc biệt là phân tích giải thuật trong việc giải quyết các bài toán tin học quy mô vừa và nhỏ.
- Nhận biết các loại cấu trúc dữ liệu cơ bản
- Ước lượng thời gian thực hiện cho một số thuật toán đơn giản
1.1 | BIẾN, KIỂU DỮ LIỆU, CẤU TRÚC DỮ LIỆU
Trước khi hiểu biến là gì, hãy liên hệ tới một biểu thức toán học rất quen thuộc:
Biểu thức toán học sử dụng tên biến (x, y) để lưu trữ giá trị, tương tự như biến (variables) trong lập trình máy tính dùng để lưu trữ dữ liệu Việc hiểu khái niệm này quan trọng hơn cách sử dụng công thức.
1.1.2 | Kiểu dữ liệu (Data types)
Biểu thức toán học sử dụng các biến x và y có thể nhận giá trị nguyên (ví dụ: 10, 20) hoặc thực (ví dụ: 0.23, 5.5) Để giải biểu thức, cần xác định kiểu dữ liệu của x và y Trong khoa học máy tính, kiểu dữ liệu phân loại dữ liệu thành các tập giá trị xác định trước như số nguyên (integer), số thực (floating point), kí tự (character), chuỗi (string), v.v.
Dung lượng bộ nhớ lưu trữ dữ liệu khác nhau tùy thuộc vào kiểu dữ liệu, ngôn ngữ lập trình, trình biên dịch và hệ điều hành Ví dụ, số nguyên thường chiếm một lượng bộ nhớ nhất định.
2 bytes hoặc 4 bytes, kiểu kí tự chiếm 1 byte (kiểu char, lưu mã ASCII của ký tự) hoặc
2 byte (kiểu wchar_t, lưu trữ dựa trên bảng mã quốc tế UTF-16 – một dạng mã unicode),
Có hai loại kiểu dữ liệu:
Kiểu dữ liệu cơ sở (System-defined data types or Primitive data types)
Kiểu dữ liệu do người dùng tự định nghĩa (User-defined data types)
Kiểu dữ liệu cơ sở
Các ngôn ngữ lập trình thường hỗ trợ các kiểu dữ liệu cơ sở như `int`, `float`, `char`, `double`, `bool` Kích thước (và do đó miền giá trị) của mỗi kiểu phụ thuộc vào ngôn ngữ, trình biên dịch và hệ điều hành.
Kiểu dữ liệu `int` có thể chiếm 2 hoặc 4 byte trong bộ nhớ Với 2 byte (16 bit), giá trị nằm trong khoảng -32,768 đến 32,767 (-215 đến 215-1) Nếu là 4 byte (32 bit), phạm vi giá trị mở rộng từ -2,147,483,648 đến 2,147,483,647 (-231 đến 231-1) Điều này cũng tương tự với các kiểu dữ liệu khác.
Kiểu dữ liệu do người dùng tự định nghĩa
Ngôn ngữ lập trình C/C++ cho phép định nghĩa kiểu dữ liệu mới bằng từ khóa `struct` để lưu trữ dữ liệu phức tạp vượt quá khả năng của kiểu dữ liệu cơ sở, đáp ứng nhu cầu xử lý bài toán thực tế Ví dụ: `struct newType`.
Cấu trúc dữ liệu là cách tổ chức và lưu trữ dữ liệu trên máy tính để tối ưu hiệu suất sử dụng Các loại cấu trúc dữ liệu phổ biến gồm mảng (arrays), tập tin (files), danh sách liên kết (linked lists), ngăn xếp (stacks), hàng đợi (queues), cây (trees) và đồ thị (graphs).
Phụ thuộc vào cách tổ chức của các phần tử mà cấu trúc dữ liệu được chia thành hai loại:
Cấu trúc dữ liệu tuyến tính (linear data structures): Các phần tử được truy cập tuần tự Ví dụ như Linked Lists, Stacks và Queues
Cấu trúc dữ liệu phi tuyến tính (non- linear data structures): các phần tử được truy cập không theo trật tự tuyến tính Ví dụ như Trees và graphs
1.2 | KIỂU DỮ LIỆU TRỪU TƯỢNG (ADTS)
Trước khi tìm hiểu về kiểu dữ liệu trừu tượng thì cần hiểu rõ về một số khái niệm sau:
Trừu tượng hóa trong thiết kế tách biệt chi tiết triển khai khỏi người dùng, cho phép sử dụng dễ dàng mà không cần hiểu biết kỹ thuật bên dưới.
Trừu tượng hóa chức năng (Function abstraction): là quá trình tách biệt cài đặt chi tiết các chức năng với cách sử dụng các chức năng đó
Trừu tượng hóa dữ liệu tách biệt cài đặt chi tiết cấu trúc dữ liệu khỏi cách sử dụng Kiểu dữ liệu cơ sở (int, float) có sẵn các thao tác (+, -, ), nhưng kiểu dữ liệu người dùng định nghĩa cần tự định nghĩa các thao tác tương ứng Tóm lại, khi định nghĩa kiểu dữ liệu mới, cần định nghĩa luôn các thao tác hợp lệ kèm theo.
Abstract Data Types (ADTs) combine data structures with operations on those structures—a fusion of data abstraction and function abstraction An ADT consists of two parts: [The second part needs to be specified to complete the paragraph.]
Một vài ADTs thông dụng như: Linked Lists, Stacks, Queues, Priority Queues, Binary Trees,
1.3 | KHÁI NIỆM GIẢI THUẬT VÀ PHÂN TÍCH GIẢI THUẬT
Giải thuật là tập hợp các bước cụ thể, tuần tự từ dữ liệu đầu vào đến kết quả mong muốn để giải quyết một vấn đề.
Một giải thuật là tập hợp các bước rõ ràng, xác định để giải quyết một bài toán cho trước
Một thuật toán tốt được đánh giá dựa trên hai tiêu chí chính: tính chính xác (cho lời giải trong số bước hữu hạn) và tính hiệu quả (tiết kiệm tài nguyên, thời gian và bộ nhớ).
Nhiều thuật toán giải quyết cùng một vấn đề (ví dụ: sắp xếp có insertion sort, selection sort, quick sort ) Phân tích thuật toán giúp chọn thuật toán tối ưu, so sánh hiệu quả về thời gian và bộ nhớ Thời gian chạy phụ thuộc vào nhiều yếu tố.
Phong cách của người lập trình
Tốc độ thực hiện các phép toán của máy tính
Kích cỡ dữ liệu đầu vào
Phân tích thuật toán thực nghiệm không xác định chính xác thời gian chạy vì chỉ chạy với một số dữ liệu Cỡ dữ liệu vào, thường là số nguyên dương n, quyết định thời gian chạy; ví dụ, sắp xếp mảng n phần tử Thời gian chạy tăng theo cỡ dữ liệu (n), nhưng cũng phụ thuộc vào dữ liệu cụ thể.
MỘT SỐ GIẢI THUẬT TÌM KIẾM
Khoa học máy tính sử dụng thuật toán tìm kiếm để truy xuất hiệu quả lượng thông tin khổng lồ được lưu trữ Chương này trình bày hai thuật toán tìm kiếm cơ bản phổ biến hiện nay.
- Cài đặt một số giải thuật tìm kiếm
Tìm kiếm là quá trình xác định vị trí một phần tử hoặc thuộc tính của nó trong một tập dữ liệu, bao gồm cơ sở dữ liệu, mảng, file văn bản, cây, hay đồ thị.
Việc tìm kiếm các đối tượng chủ yếu dựa vào các khóa, do vậy thường thì bài toán tìm kiếm được diễn ra trên bảng khóa
Tìm kiếm tuần tự trong mảng số nguyên hoặc danh sách đối tượng (sinh viên với khóa là mã số hoặc họ tên) là một thuật toán cơ bản Khóa tìm kiếm thường là mã sinh viên hoặc họ tên để tối ưu hiệu quả.
Tối ưu hóa tìm kiếm dữ liệu phụ thuộc vào phương pháp lưu trữ Hai thuật toán tìm kiếm phổ biến là tìm kiếm tuyến tính (tuần tự) và tìm kiếm nhị phân (dành cho dữ liệu đã sắp xếp).
Bài viết trình bày thuật toán tìm kiếm trong mảng một chiều Dữ liệu được lưu trữ dưới dạng mảng nguyên `int arrInt[n]` chứa n phần tử (a1, a2, , an) Mục tiêu là tìm kiếm khóa `key` (kiểu nguyên) trong mảng.
Khi tìm kiếm trong mảng dữ liệu phức tạp, ví dụ mảng `SinhVien arr[n]`, khóa tìm kiếm (như họ tên: `string hoTen`) có thể khác kiểu dữ liệu với phần tử mảng, chỉ cần trùng khớp với một trường dữ liệu cụ thể của phần tử đó.
Output: Trả về vị trí tìm thấy key trong dãy (nếu tìm thấy) Nếu không tìm thấy thì trả về -1
Giải thuật tìm kiếm tuần tự hoạt động hiệu quả với mọi dãy, dù đã sắp xếp hay chưa Với dãy chưa sắp xếp, cần duyệt toàn bộ để xác định sự tồn tại của khóa tìm kiếm.
Mô phỏng tìm kiếm trên dãy số nguyên:
- Trường hợp tìm thấy khóa key = 8
- Trường hợp không tìm thấy khóa key = 8
Tìm kiếm tên sinh viên trong danh sách n sinh viên có độ phức tạp thời gian O(n), trường hợp xấu nhất cần duyệt toàn bộ danh sách.
Tìm kiếm trong mảng đã sắp xếp hiệu quả hơn vì ta có thể dừng tìm kiếm khi phần tử cần tìm nhỏ hơn phần tử đang xét, tránh quét toàn bộ mảng.
Tìm kiếm tuyến tính có độ phức tạp thời gian O(n) trong trường hợp xấu nhất (khóa ở cuối hoặc không tồn tại), nhưng hiệu quả hơn trong trường hợp trung bình so với các phương pháp khác.
Tìm kiếm tuần tự trong danh sách đã sắp xếp kém hiệu quả khi khóa cần tìm nằm cuối danh sách hoặc danh sách rất lớn Để tối ưu, thuật toán tìm kiếm nhị phân (mục 2.3) là giải pháp hiệu quả hơn.
Thuật toán tìm kiếm nhị phân hoạt động dựa trên nguyên tắc chia đôi phạm vi tìm kiếm Ban đầu, so sánh từ cần tìm với từ ở giữa cuốn từ điển Nếu trùng khớp, tìm kiếm kết thúc Nếu nhỏ hơn, tìm tiếp ở nửa trái; nếu lớn hơn, tìm ở nửa phải, lặp lại quá trình cho đến khi tìm thấy hoặc hết phạm vi.
Mô phỏng: với khóa nKey = 8
Sau đây là hàm tìm kiếm nhị phân áp dụng trên dãy số nguyên được sắp xếp tăng dần // Binary Search Algorithm int binarySearch (int arrInt[], int n, int nKey)
{ int nLeft = 0; int nRight = n – 1; int nMid; while (nLeft