Khi bắt đầu làm quen với ngôn ngữ lập trình – Cụ thể là ngôn ngữ C – Sinh Viên thường gặp khó khăn trong việc chuyển vấn đề lý thuyết sang cài đặt cụ thể trên máy. Sách “Giáo Trình Bài Tập Kỹ Thuật Lập Trình” nhằm cung cấp cho các Học Sinh Sinh Viên Trường CĐ Công Nghệ Thông Tin Tp. Hồ Chí Minh hệ thống các bài tập, những kỹ năng thực hành cơ bản và nâng cao về ngôn ngữ lập trình C.Khi bắt đầu làm quen với ngôn ngữ lập trình – Cụ thể là ngôn ngữ C – Sinh Viên thường gặp khó khăn trong việc chuyển vấn đề lý thuyết sang cài đặt cụ thể trên máy. Sách “Giáo Trình Bài Tập Kỹ Thuật Lập Trình” nhằm cung cấp cho các Học Sinh Sinh Viên Trường CĐ Công Nghệ Thông Tin Tp. Hồ Chí Minh hệ thống các bài tập, những kỹ năng thực hành cơ bản và nâng cao về ngôn ngữ lập trình C.
Trang 1KỸ THUẬT LẬP TRÌNH
Nguyễn Mạnh Sơn
Email: manhsoncntt@yahoo.com
Trang 2Nội dung
móc nối
Trang 3Chương 1: Đại cương về lập trình cấu trúc
Trang 4Lịch sử lập trình cấu trúc
Trang 5Cấu trúc lệnh
Trang 6Cấu trúc lệnh
Trang 8Cấu trúc dữ liệu
Các cấu trúc dữ liệu được phân thành hai loại:
Cấu trúc dữ liệu có kiểu cơ bản (Base type), bao gồm:
Kiểu kí tự (char)
Kiểu số nguyên có dấu (signed int)
Kiểu số nguyên không dấu (unsigned int)
Kiểu số nguyên dài có dấu (signed long)
Kiểu số nguyên dài không dấu (unsigned long )
Kiểu số thực (float)
Kiểu số thực có độ chính xác gấp đôi (double)
Cấu trúc dữ liệu có kiểu do người dùng định nghĩa (User type) hay còn gọi là kiểu dữ liệu có cấu trúc, bao gồm:
Kiểu xâu kí tự (string)
Kiểu mảng (array)
Kiểu tập hợp (union),
Kiểu cấu trúc (struct),
Kiểu file,
Kiểu con trỏ (pointer)
Các kiểu dữ liệu được định nghĩa mới hoàn toàn như kiểu danh sách móc nối (link list), kiểu cây (tree)
Trang 9Nguyên lý tối thiểu
??
Trang 10Nguyên lý địa phương
Các biến địa phương trong hàm, thủ tục hoặc chu trình cho dù có trùng tên với biến toàn cục thì khi xử lý biến
đó trong hàm hoặc thủ tục vẫn không làm thay đổi giá trị của biến toàn cục.
Tên của các biến trong đối của hàm hoặc thủ tục đều là hình thức.
Mọi biến hình thức truyền theo trị cho hàm hoặc thủ tục đều là các biến địa phương.
Các biến khai báo bên trong các chương trình con, hàm hoặc thủ tục đều là biến địa phương.
Khi phải sử dụng biến phụ nên dùng biến địa phương và hạn chế tối đa việc sử dụng biến toàn cục để tránh xảy ra các hiệu ứng phụ.
Trang 11Nguyên lý nhất quán
Dữ liệu thế nào thì phải thao tác thế ấy
trúc dữ liệu và thao tác để kịp thời khắc phục.
Trang 12Nguyên lý an toàn
Lỗi nặng nhất nằm ở mức cao nhất (mức ý đồ thiết kế)
và ở mức thấp nhất thủ tục phải chịu tải lớn nhất.
Mọi lỗi, dù là nhỏ nhất cũng phải được phát hiện ở một bước nào đó của chương trình Quá trình kiểm tra và
phát hiện lỗi phải được thực hiện trước khi lỗi đó hoành hành
Các loại lỗi thường xảy ra trong khi viết chương trình:
Lỗi xảy ra trong quá trình liên kết
Trang 13Phương pháp top-down
Quá trình phân tích bài toán được thực hiện từ trên
xuống dưới Từ vấn đề chung nhất đến vấn đề cụ thể
nhất Từ mức trừu tượng mang tính chất tổng quan tới mức đơn giản nhất là đơn vị chương trình.
Quá trình phân rã bài toán được thực hiện theo từng mức khác nhau
Mức thấp nhất được gọi là mức tổng quan (level 0), mức tổng
quan cho phép ta nhìn tổng thể hệ thống thông qua các chức
năng của nó, nói cách khác mức 0 sẽ trả lời thay cho câu hỏi “Hệ thống có thể thực hiện được những gì ?”
Mức tiếp theo là mức các chức năng chính Ở mức này, những chức năng cụ thể được mô tả Một hệ thống có thể được phân tích thành nhiều mức khác nhau, mức thấp được phép sử dụng các dịch vụ của mức cao
Quá trình phân tích tiếp tục phân rã hệ thống theo từng chức
năng phụ cho tới khi nào nhận được mức các đơn thể ( UNIT,
Function, Procedure), khi đó chúng ta tiến hành cài đặt hệ thống
Trang 14cho quá trình cài đặt hệ thống
Trang 15Chương 2: Duyệt và đệ quy
Trang 16 f(n+1) = ( n+1) f(n) với mọi n nguyên dương.
Cấu trúc tự trỏ được định nghĩa bằng đệ qui
struct node { int infor;
struct node *left;
struct node *right;
};
Trang 17Giải thuật đệ quy
Một thuật toán được gọi là đệ qui nếu nó giải bài toán
bằng cách rút gọn bài toán ban đầu thành bài toán tương
tự như vậy sau một số hữu hạn lần thực hiện.
Trong mỗi lần thực hiện, dữ liệu đầu vào tiệm cận tới tập
Trang 18Giải thuật đệ quy
Thuật toán 2: Thuật toán đệ qui tính ước số chung lớn nhất của hai số
Thuật toán 3: Thuật toán đệ qui tính n!
long factorial( int n){
if (n ==1)
return(1);
return(n * factorial(n-1));
}
Thuật toán 4: Thuật toán đệ qui tính số fibonacci thứ n
int fibonacci( int n) {
if (n==0) return(0);
else if (n ==1) return(1);
return(fibonacci(n-1) + fibonacci(n-2));
}
Trang 19Phương pháp sinh
Điều kiện:
Có thể xác định được một thứ tự trên tập các cấu hình tổ hợp
cần liệt kê Từ đó có thể xác định được cấu hình tổ hợp đầu tiên và cuối cùng trong thứ tự đã được xác định
Xây dựng được thuật toán từ cấu hình chưa phải là cuối cùng
đang có để đưa ra cấu hình kế tiếp sau nó
Thuật toán tổng quát
void Generate(void){
<Xây dựng cấu hình ban đầu>;
stop =falsewhile (not stop) {
<Đưa ra cấu hình đang có>;
Sinh_Kế_Tiếp;
}}
Trang 20Liệt kê các xâu nhị phân độ dài n
Quy tắc
Tìm i đầu tiên từ phải xang trái (i=n, n-1, ,1) thoả mãn bi =0
Gán lại bi =1 và bj=0 với tất cả j>i Dãy thu được là dãy cần tìm
Trang 21Thuật toán quay lui
Nội dung chính của thuật toán
Xây dựng dần các thành phần của cấu hình bằng cách thử tất cả các khả năng Với mỗi khả năng j, kiểm tra xem j có chấp nhận được hay không Khi đó có thể xảy ra hai trường hợp:
Nếu chấp nhận j thì xác định xi theo j, nếu i=n thì ta được một cấu hình cần tìm, ngược lại xác định tiếp thành phần x i+1
Nếu thử tất cả các khả năng mà không có khả năng nào được chấp nhận thì quay lại bước trước đó để xác định lại x i-1
Giải thuật tổng quát
void Try( int i ) {
Trang 22Đặt hậu;
if i<8 {
DatHau(i+1);
if Không thành công
Bỏ hậu đã đặt ra khỏi vị trí }
} }while (Không thành công) && (Vẫn còn lựa chọn)
}
Trang 23}
Trang 24Thuật toán nhánh cận
Trang 29Ngăn xếp – Các thao tác
Thao tác Pop : Loại bỏ nút tại đỉnh stack.
int Pop ( stack *ps) {
if (Empty(ps) {
printf(“\n stack empty”);
return(0);
} return( ps -> nodes[ps->top ]);
}
Trang 30Ứng dụng
Quá trình đảo ngược một xâu kí tự giống như việc đưa vào
(push) từng kí tự trong xâu vào stack, sau đó đưa ra (pop) các kí
tự trong stack ra cho tới khi stack rỗng ta được một xâu đảo
Biểu thức dạng hậu tố chỉ chứa các phép toán cộng (+), trừ (-), nhân (*), chia (/), lũy thừa ($)
Ví dụ : 23+5*2$ = ( (2 + 3) *5 ) 2 = 625
Trang 32Hàng đợi
Trang 33 Người sản xuất có thể sản xuất tối đa n mặt hàng
Người tiêu dùng chỉ được phép sử dụng trong số n mặt hàng
Tuy nhiên, người sản xuất chỉ có thể lưu trữ vào kho khi và chỉ khi kho chưa bị đầy
Ngược lại, nếu kho hàng không rỗng (kho có hàng) người tiêu
dùng có thể tiêu dùng những mặt hàng trong kho theo nguyên tắc hàng nào nhập vào kho trước được tiêu dùng trước giống như
cơ chế FIFO của queue
Trang 34Hàng đợi – Các thao tác
hàng đợi Ở trạng thái này, font và rear có cùng một giá trị MAX-1.
void Initialize ( queue *pq){
pq->front = pq->rear = MAX -1;
}
hay không Hàng đợi rỗng khi front == rear.
Trang 35Hàng đợi – Các thao tác
Thao tác Insert: thêm X vào hàng đợi Q Nếu việc thêm X vào
hàng đợi được thực hiện ở đầu hàng, khi đó rear có giá trị 0, nếu rear không phải ở đầu hàng đợi thì giá trị của nó được tăng lên 1 đơn vị
void Insert(queue *pq, hang x){
Trang 36Hàng đợi – Các thao tác
Thao tác Remove: loại bỏ phần tử ở vị trí front khỏi hàng đợi Nếu
hàng đợi ở trạng thái rỗng thì thao tác Remove không thể thực hiện được, trong trường hợp khác front được tăng lên một đơn vị
Trang 37Hàng đợi – Các thao tác
Thao tác Traver: Duyệt tất cả các nút trong hàng đợi.
void Traver( queue *pq){
Trang 38 Mỗi đỉnh trong danh sách đều gồm hai phần
Phần thứ nhất chứa dữ liệu Dữ liệu có thể chỉ là một biến đơn hoặc
là một cấu trúc (hoặc con trỏ cấu trúc) có kiểu nào đó
Phần thứ hai của đỉnh là một con trỏ chỉ vào địa chỉ của đỉnh tiếp theo trong danh sách
Trang 39Danh sách liên kết đơn
NODEPTR Freenode( NODEPTR p){
free(p);
}
Trang 40Danh sách liên kết đơn
Chèn vào đầu danh sách
void Push_Top( NODEPTR *plist, int x) {
NODEPTR p;
p= Getnode(); // cấp không gian nhớ cho đỉnh mới
p -> infor = x; // gán giá trị thích hợp cho đỉnh mới
p ->next = *plist;
*plist = p; // thiết lập liên kết
}
Trang 41Danh sách liên kết đơn
Trang 42Danh sách liên kết kép
Trang 43CHƯƠNG 4:
CẤU TRÚC DỮ LIỆU CÂY
Trang 44Cây là gì
Khái niệm cây
Cây là một tập hợp các nút (các đỉnh) và các cạnh, thỏa mãn một số yêu cầu nào đó Mỗi nút của cây đều có 1 định danh và có thể mang thông tin nào đó Các cạnh dùng để liên kết các nút với nhau Một đường đi trong cây là một danh sách các đỉnh phân biệt mà đỉnh trước có liên kết với đỉnh sau
Một tính chất rất quan trọng hình thành nên cây, đó là có đúng một
đường đi nối 2 nút bất kỳ trong cây
Định nghĩa cây dưới dạng đệ quy
Một nút đứng riêng lẻ (và nó chính là gốc của cây này).
Hoặc một nút kết hợp với một số cây con bên dưới.
Một số khái niệm khác
Mỗi nút trong cây (trừ nút gốc) có đúng 1 nút nằm trên nó, gọi là nút cha (parent) Các nút nằm ngay dưới nút đó được gọi là các nút con
(subnode ) Các nút nằm cùng cấp được gọi là các nút anh em (sibling).
Nút không có nút con nào được gọi là nút lá (leaf) hoặc nút tận cùng
Chiều cao của nút là đường đi dài nhất từ nút tới một lá Chiều cao của cây chính là chiều cao của nút gốc Độ sâu của 1 nút là độ dài đường đi duy nhất giữa nút gốc và nút đó.
Trang 45Cài đặt cây
Cài đặt cây bằng mảng các nút cha
Trang 46Cài đặt cây
Cài đặt cây thông qua danh sách các nút
con
Trang 48Duyệt cây
Duyệt là gì:
một cây theo một trình tự nào đó Trong quá trình
duyệt, tại mỗi nút ta có thể tiến hành một thao tác xử
lý nào đó Đối với các danh sách liên kết, việc duyệt qua danh sách đơn giản là đi từ nút đầu, qua các liên kết và tới nút cuối cùng
Trang 52Cây nhị phân
nó chỉ có nhiều nhất là 2 nút con Khi đó, 2 cây con của mỗi nút được gọi là cây con trái và cây con phải
Trang 53Cây nhị phân
Một số dạng cây nhị phân đặc
biệt:
Cây nhị phân đầy đủ:
Là cây nhị phân mà mỗi nút không phải lá đều có đúng 2 nút con và các nút lá phải có cùng độ sâu
Cây nhị phân tìm kiếm:
Là cây nhị phân có tính chất khóa của nút con bên trái bao giờ cũng nhỏ hơn khóa của nút cha, còn khóa của cây con bên phải bao giờ cũng lớn hơn hoặc bằng khóa của nút
Trang 54Cài đặt cây nhị phân bằng mảng
Đối với cây nhị phân không cân bằng, có thể sử dụng cấu trúc sau:
Trang 56struct node *left;
struct node *right;
}
typedef struct node *NODEPTR
Trang 57Các thao tác trên cây nhị phân
free(p);
}
void Initialize(NODEPTR *ptree){
*ptree=NULL;
}
Trang 58Các thao tác trên cây nhị phân
Kiểm tra tính rỗng của cây nhị phân:
int Empty(NODEPTR *ptree){
if (*ptree==NULL) return(TRUE);
return(FALSE);
}
Tạo một node lá cho cây nhị phân:
Cấp phát bộ nhớ cho node;
Gán giá trị thông tin thích hợp cho node;
Tạo liên kết cho node lá;
NODEPTR Makenode(int x){
NODEPTR p;
p= Getnode();// cấp phát bộ nhớ cho node
p ->infor = x; // gán giá trị thông tin thích hợp
p ->left = NULL; // tạo liên kết trái của node lá
p ->right = NULL;// tạo liên kết phải của node lá
return(p);
}
Trang 59Các thao tác trên cây nhị phân
Tạo node con bên trái của cây nhị phân:
Để tạo được node con bên trái là node lá của node p, chúng ta thực hiện như sau:
Nếu node p không có thực (p==NULL), ta không thể tạo được node con bên trái của node p;
Nếu node p đã có node con bên trái (p->left!=NULL), thì chúng ta cũng không thể tạo được node con bên trái node p;
Nếu node p chưa có node con bên trái, thì việc tạo node con bên trái chính là thao tác make node đã được xây dựng như trên;
void Setleft(NODEPTR p, int x ){
if (p==NULL){
// nếu node p không có thực thì không thể thực hiện được
printf(“\n Node p không có thực”);
delay(2000); return;
}
// nếu node p có thực và tồn tại lá con bên trái thì cũng không thực hiện được
else if ( p ->left !=NULL){
printf(“\n Node p đã có node con bên trái”);
Trang 60Các thao tác trên cây nhị phân
Tạo node con bên phải của cây nhị phân:
Để tạo được node con bên phải là node lá của node p, chúng ta làm như sau:
Nếu node p không có thực (p==NULL), thì ta không thể thực hiện được thao tác thêm node lá vào node phải node p;
Nếu node p có thực (p!=NULL) và đã có node con bên phải thì thao tác cũng không thể thực hiện được;
Nếu node p có thực và chưa có node con bên phải thì việc tạo node con bên phải node p được thực hiện thông qua thao tác Makenode();
void Setright(NODEPTR p, int x ){
if (p==NULL){ // Nếu node p không có thực
printf(“\n Node p không có thực”);
delay(2000); return;
}
// Nếu node p có thực & đã có node con bên phải
else if ( p ->right !=NULL){
printf(“\n Node p đã có node con bên phải”);
Trang 61Các thao tác trên cây nhị phân
Trang 6363
Trang 65Các khái niệm cơ bản (tiếp)
Một cạnh (u, v) của đồ thị có hướng có thể được biểu thị dạng u v Đỉnh u khi đó được gọi là đỉnh kề của v Cạnh (u, v) được gọi là cạnh xuất phát từ u Ta ký hiệu A(u) là tập các cạnh xuất phát từ u
Bậc ngoài của 1 đỉnh là số các cạnh xuất phát từ đỉnh đó Do đó, bậc ngoài của u = | A(u) |
Bậc trong của 1 đỉnh là số các cạnh đi tới đỉnh đó Do đó, bậc trong của v = | I(v) |
Một đường đi trong đồ thị có hướng G(V, E) là một chuỗi các đỉnh
P = {v1, v2, …, vk}
Trong đó, vi V (i = 1 k), và (vi, vi+1) E (i = 1 k-1)
Độ dài của đường đi trong trường hợp này là k - 1
Chu trình là một đường đi mà đỉnh đầu và đỉnh cuối trùng nhau Đồ thị liên thông là một đồ thị mà luôn tồn tại đường đi giữa 2 đỉnh bất kì
Trang 66Các khái niệm cơ bản (tiếp)
Đồ thị vô hướng là đồ thị có các cạnh không có hướng Hai nút
ở hai đầu của cạnh có vai trò như nhau Định nghĩa về đồ thị vô hướng như sau:
Đồ thị vô hướng G = <V, E> bao gồm:
V là một tập hữu hạn các đỉnh
E là một tập hữu hạn các cặp đỉnh phân biệt của V, gọi là
các cạnh
Ví dụ, đồ thị có hướng G2 = <V2, E2>, với V2 và E2
được xác định như sau:
V2 = {a, b, c, d}
E2 = {(a, b); (a, c}; (b, d); (c, b) } a b
Trang 67Ninh Bình 96km
45km
Trang 68Biểu diễn đồ thị - Ma trận kề
Đồ thị có trọng số
Trang 70 Nếu có nhiều thành phần liên thông
for( i=1; in; i++)
daxet[i] = 0;
for( i:=1;i n; i++)
if (daxet[i]=0)
DeepFirstSearch(i);
Trang 71}
Trang 72Duyệt các thành phần liên thông
Tư tưởng
Nếu số thành phần liên thông lớn hơn 1 chúng ta có thể tách
chúng thành những đồ thị con liên thông => số thành phần liên thông của nó bằng số lần gọi tới thủ tục DFS() hoặc BFS()
Để xác định số các thành phần liên thông của đồ thị, chúng ta sử dụng biến mới solt để nghi nhận các đỉnh cùng một thành phần liên thông trong mảng chuaxet[] như sau:
Nếu đỉnh i chưa được duyệt, chuaxet[i] có giá trị 0;
Nếu đỉnh i được duyệt thuộc thành phần liên thông thứ
j=solt, ta ghi nhận chuaxet[i]=solt;
Các đỉnh cùng thành phần liên thông nếu chúng có cùng giá trị trong mảng chuaxet[]
Trang 73u <= queue; /*nạp u vào hàng đợi*/
solt = solt+1; chuaxet[u] = solt; /*solt là biến toàn cục thiết lập giá trị 0*/ while (queue ) {
queue<=p; /* lấy p ra từ stack*/
for v ke(p) {
if (chuaxet[v] ) {
v<= queue; /*nạp v vào hàng đợi*/
chuaxet[v] = solt; /* v có cùng thành phần liên thông với p*/
} }
}
}
Trang 74for( i=1; i<=solt;i++){
/* Đưa ra thành phần liên thông thứ i*/
for( j=1; j<=n;j++){
if( chuaxet[j]==i)
<đưa ra đỉnh j>;
} }
}
Trang 75 Kết quả thể hiện trong mảng chuaxet[] Nếu chuaxet[t] = False thì
có nghĩa t cùng thành phần liên thông với s Ngược lại chuaxet[t]
= True thì t không cùng thành phần liên thông với s
Để đưa ra đường đi: sử dụng mảng truoc[] thiết lập giá trị ban đầu
là 0 Trong quá trình duyệt, ta thay thế giá trị của truoc[v] để ghi nhận đỉnh đi trước đỉnh v trong đường đi tìm kiếm từ s đến v
Trang 76void BFS (int u){
queue = ;
u <= queue; /*nạp u vào hàng đợi*/
chuaxet[u] = false ;/* đổi trạng thái của u*/
while (queue ) { /* duyệt tới khi nào hàng đợi rỗng*/
queue<=p; /*lấy p ra từ khỏi hàng đợi*/
for (v ke(p) ) { /* đưa các đỉnh v kề với p nhưng chưa được
xét vào hàng đợi*/
if (chuaxet[v] ) { v<= queue; /*đưa v vào hàng đợi*/
chuaxet[v] = false ;/* đổi trạng thái của v*/
truoc[v]=p;
} }
} /* end while*/