Độ phức tạp không gian: Thuật toán sử dụng bộ nhớ như thế nào
Tìm hiểu cách tính độ phức tạp không gian bằng ký hiệu tiệm cận, cách các thành phần bộ nhớ như đệ quy, cấu trúc dữ liệu và không gian bổ trợ cộng dồn, và cách giảm không gian bằng kỹ thuật tại chỗ.
Độ phức tạp không gian đo mức sử dụng bộ nhớ của thuật toán khi kích thước đầu vào tăng. Vì vậy, hiểu độ phức tạp không gian rất quan trọng cho quản lý tài nguyên trong các môi trường bị giới hạn.
Trong bài viết này, tôi sẽ giải thích chi tiết độ phức tạp không gian là gì, cách tính và cách giảm thiểu. Bên cạnh đó, tôi sẽ đề cập đến các ứng dụng thực tế và cung cấp nền tảng lý thuyết.
Tìm hiểu về độ phức tạp không gian
Trước khi đi vào tính toán và tối ưu, hãy làm rõ độ phức tạp không gian thực sự là gì và vì sao bạn nên quan tâm.
Độ phức tạp không gian là gì?
Độ phức tạp không gian là tổng bộ nhớ mà thuật toán yêu cầu tương ứng với kích thước đầu vào. Nó hữu ích để phân tích dấu chân bộ nhớ trong kịch bản xấu nhất nhằm đánh giá khả năng mở rộng của hệ thống, và tổng không gian bao gồm đầu vào, phần bổ trợ và chi phí phụ trợ; trong khi không gian bổ trợ là bộ nhớ thêm, không tính đầu vào và đầu ra. Lấy ví dụ sắp xếp một mảng: Tổng không gian bao gồm chính mảng đó, còn không gian bổ trợ có thể là vùng tạm trong merge sort.
Vì sao độ phức tạp không gian quan trọng
Trong điện toán hiện đại, hiệu quả không gian thường mang tính sống còn. Bằng cách tối ưu độ phức tạp không gian, bạn đảm bảo thuật toán chạy được trên thiết bị có RAM hạn chế, như hệ nhúng trong IoT hoặc điện thoại thông minh, hay thậm chí trên hệ lớn với quy trình ngốn tài nguyên nơi từng bit RAM đều quan trọng.
Hãy nghĩ đến ứng dụng di động: tối ưu độ phức tạp không gian giúp tránh sập ứng dụng khi xử lý ảnh. Hoặc nghĩ đến tiết kiệm chi phí lưu trữ trên đám mây, hay truyền dữ liệu và thực hiện phân tích thời gian thực mà không cần tải toàn bộ dữ liệu.
Phân tích tiệm cận và ký hiệu Big O cho không gian
Trong phần này, tôi sẽ giải thích cách các ký hiệu tiệm cận như Big O mô tả sự tăng trưởng không gian, và đưa cho bạn công cụ để chặn trên và so sánh mức sử dụng bộ nhớ.
Giới thiệu về ký hiệu Big O
Về mặt toán học, một hàm f(n) bị chi phối bởi một hàm g(n) nếu tồn tại hằng số c và k, sao cho với mọi k>n, 0 ≤ |f(n)| ≤ |g(n)|.
Khi đó, ta viết f(n)=O(g(n)), và nói f(n) là big O của g(n).
Nói đơn giản, điều này có nghĩa là f(n) không tăng nhanh hơn g(n) kể từ một chỉ số nào đó.
Ký hiệu Big O biểu diễn giới hạn trên của không gian như một hàm theo kích thước đầu vào n, để chúng ta tập trung vào các hạng chi phối khi n lớn. Ký hiệu này giúp phân loại thuật toán; ví dụ, O(n) nghĩa là bộ nhớ tăng gấp đôi khi đầu vào tăng gấp đôi.
Có một hiểu lầm phổ biến ở người mới bắt đầu rằng Big O cho không gian bỏ qua các hằng số như 2n là O(n), nhưng trên thực tế, các hằng số có ý nghĩa với n nhỏ.
Các ký hiệu tiệm cận liên quan
Có nhiều cách mô hình hóa độ phức tạp và nhiều ký hiệu khác nhau. Ví dụ, Big Omega cung cấp cận dưới nên hữu ích để chứng minh mức không gian tối thiểu, như sắp xếp đòi hỏi ít nhất Ω(n). Một ký hiệu khác là Big Theta, cho cận chặt khi cận trên và dưới trùng nhau, lý tưởng cho phân tích chính xác, như O(n) = Θ(n) cho không gian tuyến tính.
Các quy ước ký hiệu khác
Có một vài quy ước ký hiệu khác liên quan trong tài liệu về độ phức tạp không gian, như Soft-O Õ, bỏ qua các thừa số đa log. Nó phổ biến trong các thuật toán nâng cao như xử lý đồ thị, nơi các log không đáng kể. Ký hiệu này xuất hiện trong các bài báo lý thuyết cho độ phức tạp xấp xỉ như Õ cho không gian dưới tuyến tính trong thuật toán streaming.
Các thành phần của độ phức tạp không gian
Độ phức tạp không gian có nhiều yếu tố đóng góp và điểm tối ưu trong thiết kế thuật toán. Hãy xem:
Yêu cầu không gian cho đầu vào
Dữ liệu đầu vào chiếm không gian tỷ lệ với kích thước, ví dụ O(n) cho một mảng hoặc khi phân tích tập dữ liệu lớn như chuỗi gen.
Trong phỏng vấn lập trình thực tế, thường loại trừ không gian đầu vào khi tính không gian bổ trợ và chỉ tập trung vào bộ nhớ thêm mà thuật toán sử dụng. Tuy nhiên, trong lập trình thi đấu, tổng không gian thường bao gồm cả đầu vào.
Không gian bổ trợ và bộ nhớ tạm
Không gian bổ trợ là bộ nhớ thêm mà một thuật toán cần để thực hiện các thao tác của nó. Nó ảnh hưởng trực tiếp đến cách thiết kế thuật toán.
Do đó, bạn có thể, chẳng hạn, dùng O(n) cho mảng tạm trong thuật toán merge sort. Ví dụ khác là bảng băm để phát hiện trùng lặp (O(n)), ngăn xếp đệ quy cho việc khớp dấu ngoặc (O(n)), và nhiều trường hợp khác.
Các ví dụ cổ điển về không gian bổ trợ bao gồm phương pháp hai con trỏ (O(1)), thuật toán cửa sổ trượt dùng deque (O(k)), và BFS, cần O(V) không gian cho tập visited và hàng đợi.
Đoạn mã sau cho thấy ví dụ thao tác của merge sort trên mảng tạm trong bước trộn:
defmerge(left_half, right_half):
merged =[0]*(len(left_half)+len(right_half))# O(n) auxiliary space
left_idx = right_idx = merged_idx =0# merging logic...Được hỗ trợ bởi
Ngăn xếp lời gọi và không gian đệ quy
Mỗi lời gọi đệ quy thêm một khung ngăn xếp (địa chỉ trả về + tham số + biến cục bộ).
Ngược lại, đệ quy tuyến tính, như giai thừa hoặc DFS trên danh sách liên kết, có ngăn xếp O(n).
Đệ quy nhị phân, như Fibonacci ngây thơ hoặc đệ quy trên cây, có O(n) trong kịch bản xấu nhất.
Tối ưu đuôi đệ quy, được hỗ trợ trong Scheme, Rust, và đôi khi Python với decorators, giảm về O(1). Tuy nhiên, nguy cơ thực tế nằm ở giới hạn đệ quy mặc định của Python, khoảng 1000, có thể gây tràn ngăn xếp với cây sâu.
Dưới đây là ví dụ Fibonacci đệ quy ngây thơ cho thấy sự tăng của ngăn xếp:
deffibonacci(n):if n <=1:return n
return fibonacci(n-1)+ fibonacci(n-2)# O(n) stack depthĐược hỗ trợ bởi
Điều thuận lợi về chi phí phụ trợ của cấu trúc dữ liệu là tính đơn giản: mọi cấu trúc đều thêm chi phí cố định/theo phần tử, như mảng có chi phí phụ tối thiểu, danh sách liên kết có con trỏ làm tăng gấp đôi không gian, và nhiều ví dụ khác.
Dẫu vậy, ngôn ngữ lập trình có ảnh hưởng rất lớn. Danh sách Python có chi phí động, không như mảng C—ví dụ, bảng băm với hệ số tải 75% lãng phí không gian ở các ô trống.
Các loại độ phức tạp không gian
Không gian hằng số: O(1)
Độ phức tạp không gian hằng là giải pháp nền tảng và tối ưu nếu đạt được. Nhiều thuật toán quen thuộc có độ phức tạp thực sự hằng, chẳng hạn hoán đổi biến đơn giản như ví dụ dưới đây, là phép hoán đổi tại chỗ bằng cách gán tuple trong Python:
defswap_numbers(first, second):
first, second = second, first # O(1) — only two variablesreturn first, secondĐược hỗ trợ bởi
Các thuật toán không gian hằng khác đã biết gồm phân vùng cờ Hà Lan trong quicksort và duyệt cây Morris. Tuy nhiên, có giới hạn thực tế: bạn có thể có vài chục biến trước khi thấy hiệu quả; vì tùy phần cứng hệ thống, trong kịch bản xấu nhất, O(1) đôi khi vẫn không đủ nếu bộ nhớ yêu cầu vượt quá tài nguyên của hệ thống.
Không gian tuyến tính: O(n)
Không gian tuyến tính đơn giản là tỷ lệ với đầu vào. Vài ví dụ: mảng bổ sung để sao chép hoặc tập băm để lấy phần tử duy nhất. Trường hợp phổ biến nhất là trong các mẫu xử lý dữ liệu như lọc danh sách.
Không gian logarit và dưới tuyến tính
Không gian logarit cho thấy lượng bộ nhớ thuật toán dùng tăng rất chậm theo kích thước đầu vào. Cụ thể, nó tỷ lệ với logarit của kích thước đầu vào.
Ví dụ, đệ quy tìm kiếm nhị phân có độ sâu ngăn xếp O(log(n)), giống như chia để trị với đuôi đệ quy hoặc các thuật toán streaming, ước lượng kết quả trong O(log(n)) hoặc O(sqrt(n)) không gian.
Không gian logarit và dưới tuyến tính hiếm gặp, nhưng rất mạnh cho tập dữ liệu khổng lồ. Bạn có thể thấy ví dụ độ phức tạp logarit với tìm kiếm nhị phân lặp (không gian hằng) như đoạn mã dưới đây:
defbinary_search(numbers, target):
left =0
right =len(numbers)-1while left <= right:# O(1) space total
mid =(left + right)//2# comparison logic...Được hỗ trợ bởi
Không gian bậc hai: O(n²)
Không gian bậc hai xảy ra khi yêu cầu bộ nhớ tăng theo bình phương của đầu vào. Ví dụ phổ biến là bảng quy hoạch động 2 chiều có độ phức tạp O(n²) hoặc ma trận kề cho đồ thị với độ phức tạp O(V²). Tuy nhiên, cần lưu ý rằng độ phức tạp này trở nên không thực tế khi đầu vào vượt quá vài nghìn phần tử.
Không gian mũ và giai thừa
Độ phức tạp mũ, như tên gọi, làm nhu cầu bộ nhớ tăng theo mũ của kích thước đầu vào O(2^n), và điều này xảy ra khi, ví dụ, bạn lưu trữ mọi tập con hoặc mọi đường đi trên cây quyết định.
Tương tự, trong độ phức tạp giai thừa, bộ nhớ yêu cầu tỷ lệ với giai thừa của kích thước đầu vào O(n!), và điều này xảy ra khi bạn liệt kê mọi hoán vị của đầu vào. Tuy nhiên, các độ phức tạp này thường rất tốn kém và gần như chắc chắn không khả thi với n>20-30 trên hầu hết hệ thống. Nếu bạn làm việc với chúng, có thể bạn thực sự cần cắt tỉa (pruning), xấp xỉ, hoặc cách tiếp cận khác.
Cách tính độ phức tạp không gian
Phân tích có hệ thống xác định mọi cấp phát bộ nhớ và dạng tăng trưởng của chúng:
Cách tiếp cận phân tích từng dòng
Bước đầu là xem xét mã của bạn từng dòng và xác định mọi biến, mảng, và đối tượng được cấp phát. Bước tiếp theo là xác định kích thước mỗi cấp phát có phụ thuộc tham số đầu vào hay không. Sau đó, cộng bộ nhớ cho các cấp phát độc lập và lấy giá trị lớn nhất cho các nhánh loại trừ lẫn nhau. Cuối cùng, theo dõi cả cấp phát ngăn xếp cho biến cục bộ và cấp phát heap cho bộ nhớ động. Ví dụ:
deflinear_search(arr, target):
n =len(arr)# O(1) - single integerfor i inrange(n):# O(1) - loop variableif arr[i]== target:return i
return-1# Total: O(1) auxiliary spaceĐược hỗ trợ bởi
Phân tích độ phức tạp đệ quy
Độ phức tạp không gian cho đệ quy bằng độ sâu đệ quy tối đa nhân với không gian trên mỗi khung lời gọi, rồi cộng thêm mọi cấu trúc dữ liệu bổ trợ được cấp phát trong quá trình đệ quy, xác định trường hợp cơ sở, và phân tích độ sâu đệ quy liên hệ với kích thước đầu vào ra sao; ví dụ, Fibonacci ngây thơ tạo độ sâu O(n) với O(1) mỗi khung, tổng cộng O(n) không gian ngăn xếp. Dưới đây là ví dụ tính không gian cho giai thừa:
deffactorial(n):if n <=1:# Base casereturn1return n * factorial(n -1)# Space: O(n)Được hỗ trợ bởi
Phân tích độ phức tạp quy hoạch động
Các bảng DP tiêu chuẩn có kích thước phù hợp với tham số bài toán con; do đó, bài toán một chiều cần O(n) không gian, còn bài toán hai chiều cần O(nxm). Kỹ thuật tối ưu như mảng cuộn (rolling array) giảm một chiều để, chẳng hạn, biến O(n²) thành O(n). Nén trạng thái khai thác quan hệ phụ thuộc, nên nếu hàng i chỉ cần i-1, chỉ lưu hai hàng. Đây là ví dụ Fibonacci tối ưu không gian chỉ dùng hai biến:
deffibonacci_optimized(n):
previous =0
current =1for _ inrange(2, n +1):# O(1) space
previous, current = current, previous + current
return currentĐược hỗ trợ bởi
Các bước tính độ phức tạp không gian trong thực tế
Để tính độ phức tạp không gian trong thực tế, hãy làm theo các bước sau:
Làm rõ theo ngữ cảnh liệu có bao gồm không gian đầu vào hay không
Đếm các biến và hằng số kích thước cố định. Chúng đóng góp O(1)
Đo lường các cấu trúc bổ trợ như mảng và bảng băm theo kích thước đầu vào
Tính độ sâu đệ quy tối đa cho phần đóng góp của không gian ngăn xếp
Kết hợp mọi thành phần và biểu diễn bằng cận tiệm cận chặt nhất.
Độ phức tạp không gian của các thuật toán phổ biến
Yêu cầu không gian của thuật toán sắp xếp
Merge sort cần O(n) không gian bổ trợ cho các mảng tạm trong bước trộn. Trong khi đó, Quicksort chỉ dùng O(log n) không gian ngăn xếp trung bình, nhưng O(n) trong trường hợp xấu nhất nếu không tối ưu. Còn heap sort đạt O(1) không gian bổ trợ bằng cách sắp xếp tại chỗ như đoạn mã sau:
Cuối cùng, counting sort cần O(k) không gian, nơi k là miền giá trị, nhưng không thực tế khi k>>n.
Độ phức tạp không gian của thuật toán đồ thị
Duyệt đồ thị có nhiều thuật toán. Ví dụ, tìm kiếm theo chiều rộng (BFS) duy trì một hàng đợi cần O(V) không gian cho đỉnh, trong khi tìm kiếm theo chiều sâu (DFS) dùng O(V) không gian ngăn xếp ở trường hợp xấu nhất cho độ sâu tối đa. Mặt khác, thuật toán Dijkstra cần O(V) cho hàng đợi ưu tiên và theo dõi khoảng cách cho mỗi đỉnh. Cách biểu diễn đồ thị rất quan trọng vì ma trận kề dùng O(V²) trong khi danh sách kề dùng O(V+E).
Đặc tính không gian của cấu trúc dữ liệu
Có nhiều cấu trúc dữ liệu khác nhau và mỗi loại yêu cầu cấp phát bộ nhớ khác nhau. Ví dụ, Mảng và ArrayList lưu n phần tử trong bộ nhớ liên tiếp O(n), danh sách liên kết cần O(n) không gian cộng chi phí con trỏ cho mỗi nút, cây nhị phân dùng O(n) với 2-3 con trỏ mỗi nút gây chi phí đáng kể, bảng băm tiêu thụ O(n) nhân với hệ số tải thường dao động từ 1,5 đến 3 lần, và danh sách kề mang lại hiệu quả O(V+E) cho đồ thị thưa.
Ví dụ tính độ phức tạp không gian
Dưới đây là các ví dụ tính độ phức tạp không gian
# O(1): Đảo chuỗi tại chỗ
left =0
right =len(text)-1while left < right:
text[left], text[right]= text[right], text[left]# O(1)
left +=1
right -=1# O(n): Phát hiện trùng lặp bằng tập băm
seen_numbers =set()# O(n)for num in array:if num in seen_numbers:returnTrue
seen_numbers.add(num)# O(log n): Chiều cao cây nhị phân đệ quydeftree_height(root):ifnot root:return0return1+max(tree_height(root.left), tree_height(root.right))# O(log n) stack# O(n²): Ma trận khoảng cách Floyd-Warshall
distance =[[float('inf')]* vertices for _ inrange(vertices)]# O(n²)for i inrange(vertices):
distance[i][i]=0Được hỗ trợ bởi
Độ phức tạp không gian so với độ phức tạp thời gian
Thời gian và không gian là hai chiều tài nguyên khác nhau cần được cân bằng dựa trên ràng buộc hệ thống:
Khác biệt và tương đồng chính
Thoạt nhìn, độ phức tạp không gian và thời gian có vẻ giống nhau, nhưng cũng khác biệt vì độ phức tạp thời gian đo số phép tính, còn độ phức tạp không gian đo mức tiêu thụ bộ nhớ. Dù cả hai dùng cùng ký hiệu tiệm cận và khung phân tích toán học, bạn cần biết rằng thuật toán có thể tối ưu ở một chiều nhưng kém hiệu quả ở chiều kia. Tối ưu thường liên quan đến đánh đổi: tăng không gian để giảm thời gian hoặc ngược lại.
Hiểu về đánh đổi nền tảng
Khi đánh đổi không gian lấy thời gian, bạn lưu các kết quả đã tính trước để loại bỏ tính toán lặp lại. Ngược lại, đánh đổi thời gian lấy không gian yêu cầu tính lại giá trị khi cần thay vì lưu trữ. Bảng tra cứu đổi lấy khoản đầu tư không gian một lần để có truy cập O(1) lặp lại. Có thể có các ràng buộc cụ thể như giới hạn bộ nhớ và yêu cầu hiệu năng quyết định ưu tiên tài nguyên nào.
Ghi nhớ (memoization) và tối ưu quy hoạch động
Ghi nhớ hoạt động bằng cách lưu giá trị thay vì tính lại; nó hiệu quả về thời gian nhưng tốn không gian. Nó thêm, chẳng hạn, bộ nhớ đệm O(n) cho Fibonacci đệ quy, nhưng giảm thời gian từ O(2^n) xuống O(n), như ví dụ dưới đây về Fibonacci có ghi nhớ, tránh tính lại:
memory ={}deffibonacci_memory(n):if n in memo:return memory[n]# O(n) space, O(n) timeif n <=1:return n
memory[n]= fibonacci_memory(n-1)+ fibonacci_memory(n-2)return memory[n]Được hỗ trợ bởi
Quy hoạch động bottom-up, ngược lại, tính theo cách lặp để loại bỏ chi phí ngăn xếp đệ quy. Để tối ưu không gian, bạn chỉ giữ các trạng thái trước cần cho tính toán hiện tại; ví dụ, khoảng cách chỉnh sửa có thể giảm từ bảng O(nxm) xuống O(min(n,m)) bằng cách giữ một hàng.
Kỹ thuật thuật toán tại chỗ (in-place)
Thuật toán tại chỗ sửa đổi trực tiếp đầu vào thay vì cấp phát cấu trúc bộ nhớ mới, nếu lặp lại sẽ giúp đáng kể trong cấp phát bộ nhớ, từ đó giảm độ phức tạp không gian, như ví dụ dưới đây về xoay mảng tại chỗ (thuật toán đảo):
defrotate_array(nums, k):
n =len(nums)
k = k % n
reverse(nums,0, k -1)# all operations O(1) auxiliary
reverse(nums, k, n -1)
reverse(nums,0, n -1)Được hỗ trợ bởi
Các kỹ thuật tại chỗ khác giảm mạnh mức sử dụng bộ nhớ. Ví dụ, kỹ thuật hai con trỏ thao tác mảng với O(1) không gian bổ trợ, và cyclic sort xử lý bài toán hoán vị trong O(1) không gian bằng cách hoán đổi phần tử về đúng vị trí. Tuy nhiên, lưu ý rằng sửa đổi tại chỗ có thể phá hủy dữ liệu đầu vào, ảnh hưởng đến khả năng tái sử dụng và độ rõ ràng của mã.
Lựa chọn cấu trúc dữ liệu để tiết kiệm không gian
Có nhiều cấu trúc dữ liệu khác nhau, mỗi loại mang đến một đánh đổi; do đó, ví dụ, bạn nên ưu tiên mảng thay vì danh sách liên kết khi đã biết kích thước, loại bỏ chi phí con trỏ theo phần tử, và thao tác bit để lưu cờ boolean với tỷ lệ nén gấp 8 lần so với mảng byte. Trie chia sẻ tiền tố chung để tiết kiệm không gian cho bộ sưu tập chuỗi lớn. Với đồ thị thưa, hãy chọn danh sách kề thay vì ma trận để được O(V+E) thay vì O(V²) không gian.
Các lưu ý thực tiễn và hiện thực triển khai
Độ phức tạp không gian trong thế giới thực phụ thuộc vào ngôn ngữ lập trình, kiến trúc phần cứng, và môi trường runtime.
Yếu tố ngôn ngữ và môi trường runtime
Các ngôn ngữ bậc cao như Python và Java thêm siêu dữ liệu và chi phí phụ trên mỗi đối tượng, không giống C và C++, vốn cung cấp quản lý bộ nhớ thủ công cho khả năng kiểm soát chính xác đổi lại độ phức tạp. Có lẽ vì vậy các khung tối ưu hóa học máy như PyTorch dùng C++ cho tính toán.
Giới hạn kích thước ngăn xếp thường từ 1 đến 8MB, hạn chế độ sâu đệ quy tối đa. Lưu ý rằng hệ 64-bit dùng con trỏ 8 byte trong khi 32-bit dùng 4 byte, khiến kích thước cấu trúc con trỏ tăng gấp đôi. Để tìm hiểu thêm về cách dữ liệu nhiều chiều ảnh hưởng đến hiệu năng thuật toán, hãy xem hướng dẫn: Lời nguyền chiều dữ liệu trong Machine Learning.
Hệ phân cấp bộ nhớ và hiệu ứng bộ nhớ đệm
Mức sử dụng không gian và hiệu năng thuật toán phụ thuộc mạnh vào hệ phân cấp bộ nhớ. Truy cập RAM mất khoảng 100 nanogiây còn truy cập bộ nhớ đệm CPU mất từ 1 đến 10 nanogiây, nhanh hơn 100 lần. Ngoài ra, truy cập bộ nhớ tuần tự tận dụng cache line, thường 64 byte được nạp cùng nhau.
Các thuật toán có tính cục bộ không gian tốt đạt hiệu năng cao hơn nhờ hiệu quả bộ nhớ đệm. Vì vậy, khi làm việc trên tập lớn vượt quá dung lượng, sẽ gây thrashing, làm suy giảm nghiêm trọng hiệu năng.
Thu gom rác và quản lý bộ nhớ
Một công cụ hữu ích cho quản lý bộ nhớ là thu gom rác, cho phép bạn dọn dẹp dữ liệu không còn hữu ích. Nói cách khác, trình thu gom rác thêm chi phí bộ nhớ cho việc theo dõi đối tượng và quản lý thế hệ.
Đỉnh sử dụng bộ nhớ vượt quá kích thước dữ liệu đang sống do các cấp phát trước chu kỳ thu gom. Thêm nữa, GC theo thế hệ tối ưu cho đối tượng sống ngắn bằng cách giữ thế hệ trẻ trong vùng nhỏ hơn. Tuy nhiên, nếu bạn muốn kiểm soát không gian có tính quyết định hơn, bạn có thể dùng quản lý bộ nhớ thủ công, nhưng hãy nhận thức rủi ro như rò rỉ bộ nhớ và phân mảnh.
Kết luận và khuyến nghị
Cảm ơn bạn đã đọc bài viết của tôi về độ phức tạp không gian. Tôi xin để lại vài lời khuyên cuối:
Luôn làm rõ liệu phân tích không gian có bao gồm kích thước đầu vào để tránh hiểu lầm trong phỏng vấn và rà soát mã.
Ưu tiên tính đúng đắn trước, rồi chỉ tối ưu không gian khi việc đo đạc cho thấy nút thắt thực sự.
Cố gắng xem xét ràng buộc phần cứng và bộ nhớ ngay từ khâu thiết kế hệ thống để tránh tái cấu trúc tốn kém.
Luôn chọn công cụ profiling phù hợp với ngôn ngữ để xác thực phân tích lý thuyết so với hành vi thực tế và cân bằng tối ưu không gian với khả năng đọc mã. Những tối ưu quá lắt léo có thể làm hại khả năng bảo trì.
Tôi làm việc trên các hệ thống AI tăng tốc, cho phép trí tuệ ở biên với các pipeline ML liên kết (federated) trên dữ liệu phi tập trung và khối lượng công việc phân tán. Công việc của tôi tập trung vào Mô hình Lớn, Xử lý Giọng nói, Thị giác Máy tính, Học tăng cường và các cấu trúc tô-pô ML tiên tiến.
Câu hỏi thường gặp
Nếu dữ liệu thưa chỉ dùng 10% không gian O(n) đã cấp phát, độ phức tạp vẫn là O(n) chứ?⌄
Có. Độ phức tạp không gian giả định trường hợp xấu nhất. Bảo đảm tính thưa là một trường hợp bài toán khác, không phản ánh trong phân tích Big O.
Độ phức tạp không gian có thể tốt hơn độ phức tạp thời gian không?⌄
Có. Tìm kiếm tuyến tính có không gian O(1) nhưng thời gian O(n). Bạn lặp mà không lưu gì thêm.
Tại sao ghi nhớ (memoization) làm xấu đi độ phức tạp không gian dù tăng tốc thời gian?⌄
Đó là đánh đổi thời gian lấy không gian. Fibonacci cải thiện từ O(2^n) thời gian xuống O(n), nhưng thêm O(n) không gian cho bộ nhớ đệm.
Loại bỏ đuôi đệ quy có giảm độ phức tạp không gian không?⌄
Có, thực sự. Nó chuyển đệ quy thành vòng lặp, loại bỏ khung ngăn xếp. Giai thừa từ O(n) → O(1) không gian khi được tối ưu.
Nếu bảng băm dùng gấp 3× bộ nhớ do hệ số tải, độ phức tạp là O(3n) hay O(n)?⌄
O(n). Big O bỏ qua các thừa số hằng. Hệ số nhân 3x là chi phí phụ không phụ thuộc kích thước đầu vào.
Độ phức tạp không gian đo mức sử dụng bộ nhớ của thuật toán khi kích thước đầu vào tăng. Vì vậy, hiểu độ phức tạp không gian rất quan trọng cho quản lý tài nguyên trong các môi trường bị giới hạn.
Trong bài viết này, tôi sẽ giải thích chi tiết độ phức tạp không gian là gì, cách tính và cách giảm thiểu. Bên cạnh đó, tôi sẽ đề cập đến các ứng dụng thực tế và cung cấp nền tảng lý thuyết.
Tìm hiểu về độ phức tạp không gian
Trước khi đi vào tính toán và tối ưu, hãy làm rõ độ phức tạp không gian thực sự là gì và vì sao bạn nên quan tâm.
Độ phức tạp không gian là gì?
Độ phức tạp không gian là tổng bộ nhớ mà thuật toán yêu cầu tương ứng với kích thước đầu vào. Nó hữu ích để phân tích dấu chân bộ nhớ trong kịch bản xấu nhất nhằm đánh giá khả năng mở rộng của hệ thống, và tổng không gian bao gồm đầu vào, phần bổ trợ và chi phí phụ trợ; trong khi không gian bổ trợ là bộ nhớ thêm, không tính đầu vào và đầu ra. Lấy ví dụ sắp xếp một mảng: Tổng không gian bao gồm chính mảng đó, còn không gian bổ trợ có thể là vùng tạm trong merge sort.
Vì sao độ phức tạp không gian quan trọng
Trong điện toán hiện đại, hiệu quả không gian thường mang tính sống còn. Bằng cách tối ưu độ phức tạp không gian, bạn đảm bảo thuật toán chạy được trên thiết bị có RAM hạn chế, như hệ nhúng trong IoT hoặc điện thoại thông minh, hay thậm chí trên hệ lớn với quy trình ngốn tài nguyên nơi từng bit RAM đều quan trọng.
Hãy nghĩ đến ứng dụng di động: tối ưu độ phức tạp không gian giúp tránh sập ứng dụng khi xử lý ảnh. Hoặc nghĩ đến tiết kiệm chi phí lưu trữ trên đám mây, hay truyền dữ liệu và thực hiện phân tích thời gian thực mà không cần tải toàn bộ dữ liệu.
Phân tích tiệm cận và ký hiệu Big O cho không gian
Trong phần này, tôi sẽ giải thích cách các ký hiệu tiệm cận như Big O mô tả sự tăng trưởng không gian, và đưa cho bạn công cụ để chặn trên và so sánh mức sử dụng bộ nhớ.
Giới thiệu về ký hiệu Big O
Về mặt toán học, một hàm f(n) bị chi phối bởi một hàm g(n) nếu tồn tại hằng số c và k, sao cho với mọi k>n, 0 ≤ |f(n)| ≤ |g(n)|.
Khi đó, ta viết f(n)=O(g(n)), và nói f(n) là big O của g(n).
Nói đơn giản, điều này có nghĩa là f(n) không tăng nhanh hơn g(n) kể từ một chỉ số nào đó.
Ký hiệu Big O biểu diễn giới hạn trên của không gian như một hàm theo kích thước đầu vào n, để chúng ta tập trung vào các hạng chi phối khi n lớn. Ký hiệu này giúp phân loại thuật toán; ví dụ, O(n) nghĩa là bộ nhớ tăng gấp đôi khi đầu vào tăng gấp đôi.
Có một hiểu lầm phổ biến ở người mới bắt đầu rằng Big O cho không gian bỏ qua các hằng số như 2n là O(n), nhưng trên thực tế, các hằng số có ý nghĩa với n nhỏ.
Các ký hiệu tiệm cận liên quan
Có nhiều cách mô hình hóa độ phức tạp và nhiều ký hiệu khác nhau. Ví dụ, Big Omega cung cấp cận dưới nên hữu ích để chứng minh mức không gian tối thiểu, như sắp xếp đòi hỏi ít nhất Ω(n). Một ký hiệu khác là Big Theta, cho cận chặt khi cận trên và dưới trùng nhau, lý tưởng cho phân tích chính xác, như O(n) = Θ(n) cho không gian tuyến tính.
Các quy ước ký hiệu khác
Có một vài quy ước ký hiệu khác liên quan trong tài liệu về độ phức tạp không gian, như Soft-O Õ, bỏ qua các thừa số đa log. Nó phổ biến trong các thuật toán nâng cao như xử lý đồ thị, nơi các log không đáng kể. Ký hiệu này xuất hiện trong các bài báo lý thuyết cho độ phức tạp xấp xỉ như Õ cho không gian dưới tuyến tính trong thuật toán streaming.
Các thành phần của độ phức tạp không gian
Độ phức tạp không gian có nhiều yếu tố đóng góp và điểm tối ưu trong thiết kế thuật toán. Hãy xem:
Yêu cầu không gian cho đầu vào
Dữ liệu đầu vào chiếm không gian tỷ lệ với kích thước, ví dụ O(n) cho một mảng hoặc khi phân tích tập dữ liệu lớn như chuỗi gen.
Trong phỏng vấn lập trình thực tế, thường loại trừ không gian đầu vào khi tính không gian bổ trợ và chỉ tập trung vào bộ nhớ thêm mà thuật toán sử dụng. Tuy nhiên, trong lập trình thi đấu, tổng không gian thường bao gồm cả đầu vào.
Không gian bổ trợ và bộ nhớ tạm
Không gian bổ trợ là bộ nhớ thêm mà một thuật toán cần để thực hiện các thao tác của nó. Nó ảnh hưởng trực tiếp đến cách thiết kế thuật toán.
Do đó, bạn có thể, chẳng hạn, dùng O(n) cho mảng tạm trong thuật toán merge sort. Ví dụ khác là bảng băm để phát hiện trùng lặp (O(n)), ngăn xếp đệ quy cho việc khớp dấu ngoặc (O(n)), và nhiều trường hợp khác.
Các ví dụ cổ điển về không gian bổ trợ bao gồm phương pháp hai con trỏ (O(1)), thuật toán cửa sổ trượt dùng deque (O(k)), và BFS, cần O(V) không gian cho tập visited và hàng đợi.
Đoạn mã sau cho thấy ví dụ thao tác của merge sort trên mảng tạm trong bước trộn:
Ngăn xếp lời gọi và không gian đệ quy
Mỗi lời gọi đệ quy thêm một khung ngăn xếp (địa chỉ trả về + tham số + biến cục bộ).
Ngược lại, đệ quy tuyến tính, như giai thừa hoặc DFS trên danh sách liên kết, có ngăn xếp O(n).
Đệ quy nhị phân, như Fibonacci ngây thơ hoặc đệ quy trên cây, có O(n) trong kịch bản xấu nhất.
Tối ưu đuôi đệ quy, được hỗ trợ trong Scheme, Rust, và đôi khi Python với decorators, giảm về O(1). Tuy nhiên, nguy cơ thực tế nằm ở giới hạn đệ quy mặc định của Python, khoảng 1000, có thể gây tràn ngăn xếp với cây sâu.
Dưới đây là ví dụ Fibonacci đệ quy ngây thơ cho thấy sự tăng của ngăn xếp:
Xem bài blog của chúng tôi về Phân tích độ phức tạp của mã bằng Python để tìm hiểu thêm về phân tích độ phức tạp trong Python.
Chi phí phụ trợ của cấu trúc dữ liệu
Điều thuận lợi về chi phí phụ trợ của cấu trúc dữ liệu là tính đơn giản: mọi cấu trúc đều thêm chi phí cố định/theo phần tử, như mảng có chi phí phụ tối thiểu, danh sách liên kết có con trỏ làm tăng gấp đôi không gian, và nhiều ví dụ khác.
Dẫu vậy, ngôn ngữ lập trình có ảnh hưởng rất lớn. Danh sách Python có chi phí động, không như mảng C—ví dụ, bảng băm với hệ số tải 75% lãng phí không gian ở các ô trống.
Các loại độ phức tạp không gian
Không gian hằng số: O(1)
Độ phức tạp không gian hằng là giải pháp nền tảng và tối ưu nếu đạt được. Nhiều thuật toán quen thuộc có độ phức tạp thực sự hằng, chẳng hạn hoán đổi biến đơn giản như ví dụ dưới đây, là phép hoán đổi tại chỗ bằng cách gán tuple trong Python:
Các thuật toán không gian hằng khác đã biết gồm phân vùng cờ Hà Lan trong quicksort và duyệt cây Morris. Tuy nhiên, có giới hạn thực tế: bạn có thể có vài chục biến trước khi thấy hiệu quả; vì tùy phần cứng hệ thống, trong kịch bản xấu nhất, O(1) đôi khi vẫn không đủ nếu bộ nhớ yêu cầu vượt quá tài nguyên của hệ thống.
Không gian tuyến tính: O(n)
Không gian tuyến tính đơn giản là tỷ lệ với đầu vào. Vài ví dụ: mảng bổ sung để sao chép hoặc tập băm để lấy phần tử duy nhất. Trường hợp phổ biến nhất là trong các mẫu xử lý dữ liệu như lọc danh sách.
Không gian logarit và dưới tuyến tính
Không gian logarit cho thấy lượng bộ nhớ thuật toán dùng tăng rất chậm theo kích thước đầu vào. Cụ thể, nó tỷ lệ với logarit của kích thước đầu vào.
Ví dụ, đệ quy tìm kiếm nhị phân có độ sâu ngăn xếp O(log(n)), giống như chia để trị với đuôi đệ quy hoặc các thuật toán streaming, ước lượng kết quả trong O(log(n)) hoặc O(sqrt(n)) không gian.
Không gian logarit và dưới tuyến tính hiếm gặp, nhưng rất mạnh cho tập dữ liệu khổng lồ. Bạn có thể thấy ví dụ độ phức tạp logarit với tìm kiếm nhị phân lặp (không gian hằng) như đoạn mã dưới đây:
Không gian bậc hai: O(n²)
Không gian bậc hai xảy ra khi yêu cầu bộ nhớ tăng theo bình phương của đầu vào. Ví dụ phổ biến là bảng quy hoạch động 2 chiều có độ phức tạp O(n²) hoặc ma trận kề cho đồ thị với độ phức tạp O(V²). Tuy nhiên, cần lưu ý rằng độ phức tạp này trở nên không thực tế khi đầu vào vượt quá vài nghìn phần tử.
Không gian mũ và giai thừa
Độ phức tạp mũ, như tên gọi, làm nhu cầu bộ nhớ tăng theo mũ của kích thước đầu vào O(2^n), và điều này xảy ra khi, ví dụ, bạn lưu trữ mọi tập con hoặc mọi đường đi trên cây quyết định.
Tương tự, trong độ phức tạp giai thừa, bộ nhớ yêu cầu tỷ lệ với giai thừa của kích thước đầu vào O(n!), và điều này xảy ra khi bạn liệt kê mọi hoán vị của đầu vào. Tuy nhiên, các độ phức tạp này thường rất tốn kém và gần như chắc chắn không khả thi với n>20-30 trên hầu hết hệ thống. Nếu bạn làm việc với chúng, có thể bạn thực sự cần cắt tỉa (pruning), xấp xỉ, hoặc cách tiếp cận khác.
Cách tính độ phức tạp không gian
Phân tích có hệ thống xác định mọi cấp phát bộ nhớ và dạng tăng trưởng của chúng:
Cách tiếp cận phân tích từng dòng
Bước đầu là xem xét mã của bạn từng dòng và xác định mọi biến, mảng, và đối tượng được cấp phát. Bước tiếp theo là xác định kích thước mỗi cấp phát có phụ thuộc tham số đầu vào hay không. Sau đó, cộng bộ nhớ cho các cấp phát độc lập và lấy giá trị lớn nhất cho các nhánh loại trừ lẫn nhau. Cuối cùng, theo dõi cả cấp phát ngăn xếp cho biến cục bộ và cấp phát heap cho bộ nhớ động. Ví dụ:
Phân tích độ phức tạp đệ quy
Độ phức tạp không gian cho đệ quy bằng độ sâu đệ quy tối đa nhân với không gian trên mỗi khung lời gọi, rồi cộng thêm mọi cấu trúc dữ liệu bổ trợ được cấp phát trong quá trình đệ quy, xác định trường hợp cơ sở, và phân tích độ sâu đệ quy liên hệ với kích thước đầu vào ra sao; ví dụ, Fibonacci ngây thơ tạo độ sâu O(n) với O(1) mỗi khung, tổng cộng O(n) không gian ngăn xếp. Dưới đây là ví dụ tính không gian cho giai thừa:
Phân tích độ phức tạp quy hoạch động
Các bảng DP tiêu chuẩn có kích thước phù hợp với tham số bài toán con; do đó, bài toán một chiều cần O(n) không gian, còn bài toán hai chiều cần O(nxm). Kỹ thuật tối ưu như mảng cuộn (rolling array) giảm một chiều để, chẳng hạn, biến O(n²) thành O(n). Nén trạng thái khai thác quan hệ phụ thuộc, nên nếu hàng i chỉ cần i-1, chỉ lưu hai hàng. Đây là ví dụ Fibonacci tối ưu không gian chỉ dùng hai biến:
Nếu bạn tò mò về tính đa chiều và cách giảm chiều, tôi khuyến khích bạn xem các hướng dẫn của chúng tôi về Làm chủ Slowly Changing Dimensions (SCD) và Hiểu về Cardinality: Thách thức và giải pháp cho quy trình dữ liệu nặng.
Các bước tính độ phức tạp không gian trong thực tế
Để tính độ phức tạp không gian trong thực tế, hãy làm theo các bước sau:
Độ phức tạp không gian của các thuật toán phổ biến
Yêu cầu không gian của thuật toán sắp xếp
Merge sort cần O(n) không gian bổ trợ cho các mảng tạm trong bước trộn. Trong khi đó, Quicksort chỉ dùng O(log n) không gian ngăn xếp trung bình, nhưng O(n) trong trường hợp xấu nhất nếu không tối ưu. Còn heap sort đạt O(1) không gian bổ trợ bằng cách sắp xếp tại chỗ như đoạn mã sau:
Cuối cùng, counting sort cần O(k) không gian, nơi k là miền giá trị, nhưng không thực tế khi k>>n.
Độ phức tạp không gian của thuật toán đồ thị
Duyệt đồ thị có nhiều thuật toán. Ví dụ, tìm kiếm theo chiều rộng (BFS) duy trì một hàng đợi cần O(V) không gian cho đỉnh, trong khi tìm kiếm theo chiều sâu (DFS) dùng O(V) không gian ngăn xếp ở trường hợp xấu nhất cho độ sâu tối đa. Mặt khác, thuật toán Dijkstra cần O(V) cho hàng đợi ưu tiên và theo dõi khoảng cách cho mỗi đỉnh. Cách biểu diễn đồ thị rất quan trọng vì ma trận kề dùng O(V²) trong khi danh sách kề dùng O(V+E).
Đặc tính không gian của cấu trúc dữ liệu
Có nhiều cấu trúc dữ liệu khác nhau và mỗi loại yêu cầu cấp phát bộ nhớ khác nhau. Ví dụ, Mảng và ArrayList lưu n phần tử trong bộ nhớ liên tiếp O(n), danh sách liên kết cần O(n) không gian cộng chi phí con trỏ cho mỗi nút, cây nhị phân dùng O(n) với 2-3 con trỏ mỗi nút gây chi phí đáng kể, bảng băm tiêu thụ O(n) nhân với hệ số tải thường dao động từ 1,5 đến 3 lần, và danh sách kề mang lại hiệu quả O(V+E) cho đồ thị thưa.
Ví dụ tính độ phức tạp không gian
Dưới đây là các ví dụ tính độ phức tạp không gian
Độ phức tạp không gian so với độ phức tạp thời gian
Thời gian và không gian là hai chiều tài nguyên khác nhau cần được cân bằng dựa trên ràng buộc hệ thống:
Khác biệt và tương đồng chính
Thoạt nhìn, độ phức tạp không gian và thời gian có vẻ giống nhau, nhưng cũng khác biệt vì độ phức tạp thời gian đo số phép tính, còn độ phức tạp không gian đo mức tiêu thụ bộ nhớ. Dù cả hai dùng cùng ký hiệu tiệm cận và khung phân tích toán học, bạn cần biết rằng thuật toán có thể tối ưu ở một chiều nhưng kém hiệu quả ở chiều kia. Tối ưu thường liên quan đến đánh đổi: tăng không gian để giảm thời gian hoặc ngược lại.
Hiểu về đánh đổi nền tảng
Khi đánh đổi không gian lấy thời gian, bạn lưu các kết quả đã tính trước để loại bỏ tính toán lặp lại. Ngược lại, đánh đổi thời gian lấy không gian yêu cầu tính lại giá trị khi cần thay vì lưu trữ. Bảng tra cứu đổi lấy khoản đầu tư không gian một lần để có truy cập O(1) lặp lại. Có thể có các ràng buộc cụ thể như giới hạn bộ nhớ và yêu cầu hiệu năng quyết định ưu tiên tài nguyên nào.
Ghi nhớ (memoization) và tối ưu quy hoạch động
Ghi nhớ hoạt động bằng cách lưu giá trị thay vì tính lại; nó hiệu quả về thời gian nhưng tốn không gian. Nó thêm, chẳng hạn, bộ nhớ đệm O(n) cho Fibonacci đệ quy, nhưng giảm thời gian từ O(2^n) xuống O(n), như ví dụ dưới đây về Fibonacci có ghi nhớ, tránh tính lại:
Quy hoạch động bottom-up, ngược lại, tính theo cách lặp để loại bỏ chi phí ngăn xếp đệ quy. Để tối ưu không gian, bạn chỉ giữ các trạng thái trước cần cho tính toán hiện tại; ví dụ, khoảng cách chỉnh sửa có thể giảm từ bảng O(nxm) xuống O(min(n,m)) bằng cách giữ một hàng.
Kỹ thuật thuật toán tại chỗ (in-place)
Thuật toán tại chỗ sửa đổi trực tiếp đầu vào thay vì cấp phát cấu trúc bộ nhớ mới, nếu lặp lại sẽ giúp đáng kể trong cấp phát bộ nhớ, từ đó giảm độ phức tạp không gian, như ví dụ dưới đây về xoay mảng tại chỗ (thuật toán đảo):
Các kỹ thuật tại chỗ khác giảm mạnh mức sử dụng bộ nhớ. Ví dụ, kỹ thuật hai con trỏ thao tác mảng với O(1) không gian bổ trợ, và cyclic sort xử lý bài toán hoán vị trong O(1) không gian bằng cách hoán đổi phần tử về đúng vị trí. Tuy nhiên, lưu ý rằng sửa đổi tại chỗ có thể phá hủy dữ liệu đầu vào, ảnh hưởng đến khả năng tái sử dụng và độ rõ ràng của mã.
Lựa chọn cấu trúc dữ liệu để tiết kiệm không gian
Có nhiều cấu trúc dữ liệu khác nhau, mỗi loại mang đến một đánh đổi; do đó, ví dụ, bạn nên ưu tiên mảng thay vì danh sách liên kết khi đã biết kích thước, loại bỏ chi phí con trỏ theo phần tử, và thao tác bit để lưu cờ boolean với tỷ lệ nén gấp 8 lần so với mảng byte. Trie chia sẻ tiền tố chung để tiết kiệm không gian cho bộ sưu tập chuỗi lớn. Với đồ thị thưa, hãy chọn danh sách kề thay vì ma trận để được O(V+E) thay vì O(V²) không gian.
Các lưu ý thực tiễn và hiện thực triển khai
Độ phức tạp không gian trong thế giới thực phụ thuộc vào ngôn ngữ lập trình, kiến trúc phần cứng, và môi trường runtime.
Yếu tố ngôn ngữ và môi trường runtime
Các ngôn ngữ bậc cao như Python và Java thêm siêu dữ liệu và chi phí phụ trên mỗi đối tượng, không giống C và C++, vốn cung cấp quản lý bộ nhớ thủ công cho khả năng kiểm soát chính xác đổi lại độ phức tạp. Có lẽ vì vậy các khung tối ưu hóa học máy như PyTorch dùng C++ cho tính toán.
Giới hạn kích thước ngăn xếp thường từ 1 đến 8MB, hạn chế độ sâu đệ quy tối đa. Lưu ý rằng hệ 64-bit dùng con trỏ 8 byte trong khi 32-bit dùng 4 byte, khiến kích thước cấu trúc con trỏ tăng gấp đôi. Để tìm hiểu thêm về cách dữ liệu nhiều chiều ảnh hưởng đến hiệu năng thuật toán, hãy xem hướng dẫn: Lời nguyền chiều dữ liệu trong Machine Learning.
Hệ phân cấp bộ nhớ và hiệu ứng bộ nhớ đệm
Mức sử dụng không gian và hiệu năng thuật toán phụ thuộc mạnh vào hệ phân cấp bộ nhớ. Truy cập RAM mất khoảng 100 nanogiây còn truy cập bộ nhớ đệm CPU mất từ 1 đến 10 nanogiây, nhanh hơn 100 lần. Ngoài ra, truy cập bộ nhớ tuần tự tận dụng cache line, thường 64 byte được nạp cùng nhau.
Các thuật toán có tính cục bộ không gian tốt đạt hiệu năng cao hơn nhờ hiệu quả bộ nhớ đệm. Vì vậy, khi làm việc trên tập lớn vượt quá dung lượng, sẽ gây thrashing, làm suy giảm nghiêm trọng hiệu năng.
Thu gom rác và quản lý bộ nhớ
Một công cụ hữu ích cho quản lý bộ nhớ là thu gom rác, cho phép bạn dọn dẹp dữ liệu không còn hữu ích. Nói cách khác, trình thu gom rác thêm chi phí bộ nhớ cho việc theo dõi đối tượng và quản lý thế hệ.
Đỉnh sử dụng bộ nhớ vượt quá kích thước dữ liệu đang sống do các cấp phát trước chu kỳ thu gom. Thêm nữa, GC theo thế hệ tối ưu cho đối tượng sống ngắn bằng cách giữ thế hệ trẻ trong vùng nhỏ hơn. Tuy nhiên, nếu bạn muốn kiểm soát không gian có tính quyết định hơn, bạn có thể dùng quản lý bộ nhớ thủ công, nhưng hãy nhận thức rủi ro như rò rỉ bộ nhớ và phân mảnh.
Kết luận và khuyến nghị
Cảm ơn bạn đã đọc bài viết của tôi về độ phức tạp không gian. Tôi xin để lại vài lời khuyên cuối: