"Thời gian khấu hao không đổi" nghĩa là gì khi nói về độ phức tạp thời gian của thuật toán?
"Thời gian khấu hao không đổi" nghĩa là gì khi nói về độ phức tạp thời gian của thuật toán?
Câu trả lời:
Thời gian khấu hao được giải thích bằng các thuật ngữ đơn giản:
Nếu bạn thực hiện một thao tác một triệu lần, bạn không thực sự quan tâm đến trường hợp xấu nhất hoặc trường hợp tốt nhất của hoạt động đó - điều bạn quan tâm là tổng thời gian được thực hiện khi bạn lặp lại thao tác một triệu lần .
Vì vậy, sẽ không có vấn đề gì nếu hoạt động rất chậm một lần, miễn là "thỉnh thoảng" hiếm khi đủ để làm chậm sự chậm trễ. Về cơ bản thời gian khấu hao có nghĩa là "thời gian trung bình được thực hiện cho mỗi thao tác, nếu bạn thực hiện nhiều thao tác". Thời gian khấu hao không phải là hằng số; bạn có thể có thời gian khấu hao tuyến tính và logarit hoặc bất cứ điều gì khác.
Hãy lấy ví dụ về thảm của một mảng động mà bạn liên tục thêm các mục mới. Thông thường việc thêm một mục mất thời gian không đổi (nghĩa là, O(1)
). Nhưng mỗi khi mảng đầy, bạn phân bổ gấp đôi dung lượng, sao chép dữ liệu của bạn vào vùng mới và giải phóng không gian cũ. Giả sử phân bổ và giải phóng chạy trong thời gian không đổi, quá trình mở rộng này mất O(n)
thời gian trong đó n là kích thước hiện tại của mảng.
Vì vậy, mỗi lần bạn phóng to, bạn mất khoảng gấp đôi thời gian so với lần phóng to cuối cùng. Nhưng bạn cũng đã đợi gấp đôi thời gian trước khi làm điều đó! Do đó, chi phí cho mỗi lần mở rộng có thể được "trải ra" trong số các lần chèn. Điều này có nghĩa là về lâu dài, tổng thời gian dành cho việc thêm m các mục vào mảng là O(m)
và do đó thời gian khấu hao (tức là thời gian cho mỗi lần chèn) là O(1)
.
Điều đó có nghĩa là theo thời gian, trường hợp xấu nhất sẽ được mặc định là O (1) hoặc thời gian không đổi. Một ví dụ phổ biến là mảng động. Nếu chúng ta đã phân bổ bộ nhớ cho một mục mới, thêm nó sẽ là O (1). Nếu chúng tôi chưa phân bổ, chúng tôi sẽ làm như vậy bằng cách phân bổ, gấp đôi số tiền hiện tại. Chèn cụ thể này sẽ không phải là O (1), mà là một cái gì đó khác.
Điều quan trọng là thuật toán đảm bảo rằng sau một chuỗi các hoạt động, các hoạt động đắt tiền sẽ được khấu hao và từ đó hiển thị toàn bộ hoạt động O (1).
Hoặc trong điều khoản nghiêm ngặt hơn,
Có một hằng số c, sao cho mỗi chuỗi hoạt động (cũng là một kết thúc với một hoạt động tốn kém) có độ dài L, thời gian không lớn hơn c * L (Cảm ơn Rafał Dowgird )
Để phát triển cách suy nghĩ trực quan về nó, hãy xem xét chèn các phần tử trong mảng động (ví dụ std::vector
trong C ++). Hãy vẽ biểu đồ, cho thấy sự phụ thuộc của số lượng hoạt động (Y) cần thiết để chèn N phần tử vào mảng:
Các phần dọc của đồ thị màu đen tương ứng với sự phân bổ lại bộ nhớ để mở rộng một mảng. Ở đây chúng ta có thể thấy rằng sự phụ thuộc này có thể được biểu diễn đại khái dưới dạng một dòng. Và phương trình đường này là Y=C*N + b
( C
không đổi, b
= 0 trong trường hợp của chúng tôi). Do đó, chúng ta có thể nói rằng chúng ta cần dành C*N
trung bình các hoạt động để thêm N phần tử vào mảng hoặc các C*1
hoạt động để thêm một phần tử (thời gian không đổi được khấu hao).
Tôi thấy bên dưới Wikipedia giải thích hữu ích, sau khi đọc lại 3 lần:
Nguồn: https://en.wikipedia.org/wiki/Amortized_analysis#Docate_Array
"Mảng động
Phân tích khấu hao của hoạt động đẩy cho một mảng động
Hãy xem xét một mảng động tăng kích thước khi nhiều phần tử được thêm vào nó, chẳng hạn như ArrayList trong Java. Nếu chúng tôi bắt đầu với một mảng động có kích thước 4, sẽ mất thời gian liên tục để đẩy bốn yếu tố lên nó. Tuy nhiên, việc đẩy một phần tử thứ năm lên mảng đó sẽ mất nhiều thời gian hơn vì mảng sẽ phải tạo một mảng mới gấp đôi kích thước hiện tại (8), sao chép các phần tử cũ vào mảng mới và sau đó thêm phần tử mới. Ba thao tác đẩy tiếp theo tương tự sẽ mất thời gian liên tục, và sau đó việc bổ sung tiếp theo sẽ yêu cầu tăng gấp đôi kích thước mảng chậm khác.
Nói chung, nếu chúng ta xem xét một số lần đẩy n tùy ý đến một mảng có kích thước n, chúng tôi nhận thấy rằng các hoạt động đẩy mất thời gian không đổi trừ lần cuối cùng mất O (n) để thực hiện thao tác nhân đôi kích thước. Vì có tổng số n thao tác, chúng ta có thể lấy trung bình của điều này và thấy rằng để đẩy các phần tử lên mảng động sẽ mất: O (n / n) = O (1), thời gian không đổi. "
Theo hiểu biết của tôi như một câu chuyện đơn giản:
Giả sử bạn có rất nhiều tiền. Và, bạn muốn xếp chúng lên trong một căn phòng. Và, bạn có bàn tay và chân dài, miễn là bạn cần bây giờ hoặc trong tương lai. Và, bạn phải điền tất cả vào một phòng, vì vậy thật dễ dàng để khóa nó.
Vì vậy, bạn đi thẳng đến cuối / góc của căn phòng và bắt đầu xếp chúng. Khi bạn xếp chúng, từ từ căn phòng sẽ hết chỗ. Tuy nhiên, khi bạn điền vào, thật dễ dàng để xếp chúng. Có tiền, bỏ tiền. Dễ dàng. Đó là O (1). Chúng tôi không cần phải chuyển bất kỳ khoản tiền nào trước đó.
Khi phòng hết không gian. Chúng tôi cần một phòng khác, lớn hơn. Ở đây có một vấn đề, vì chúng tôi chỉ có thể có 1 phòng nên chúng tôi chỉ có thể có 1 khóa, chúng tôi cần chuyển tất cả số tiền hiện có trong phòng đó sang phòng mới lớn hơn. Vì vậy, chuyển tất cả tiền, từ phòng nhỏ, sang phòng lớn hơn. Đó là, xếp tất cả chúng một lần nữa. Vì vậy, chúng tôi cần phải di chuyển tất cả số tiền trước đó. Vì vậy, nó là O (N). (giả sử N là tổng số tiền của tiền trước đó)
Nói cách khác, thật dễ dàng cho đến N, chỉ có 1 thao tác, nhưng khi chúng tôi cần chuyển đến một phòng lớn hơn, chúng tôi đã thực hiện N hoạt động. Vì vậy, nói cách khác, nếu chúng ta tính trung bình, đó là 1 lần chèn vào lúc bắt đầu và thêm 1 lần di chuyển trong khi chuyển sang phòng khác. Tổng cộng có 2 thao tác, một thao tác chèn, một lần di chuyển.
Giả sử N lớn như 1 triệu ngay cả trong phòng nhỏ, 2 thao tác so với N (1 triệu) không thực sự là một con số tương đương, vì vậy nó được coi là hằng số hoặc O (1).
Giả sử khi chúng ta làm tất cả những điều trên trong một căn phòng lớn hơn, và một lần nữa cần phải di chuyển. Nó vẫn vậy. giả sử, N2 (giả sử, 1 tỷ đồng) là số tiền mới trong phòng lớn hơn
Vì vậy, chúng tôi có N2 (bao gồm N của trước đó vì chúng tôi chuyển tất cả từ phòng nhỏ sang phòng lớn hơn)
Chúng tôi vẫn chỉ cần 2 thao tác, một thao tác được đưa vào phòng lớn hơn, sau đó là thao tác di chuyển khác để chuyển sang phòng lớn hơn.
Vì vậy, ngay cả đối với N2 (1 tỷ), đó là 2 thao tác cho mỗi hoạt động. mà không có gì nữa Vì vậy, nó là hằng số, hoặc O (1)
Vì vậy, khi N tăng từ N lên N2 hoặc khác, điều đó không quan trọng lắm. Nó vẫn là hằng số, hoặc các thao tác O (1) cần thiết cho mỗi N.
Bây giờ giả sử, bạn có N là 1, rất nhỏ, số tiền nhỏ và bạn có một phòng rất nhỏ, sẽ chỉ phù hợp với 1 số tiền.
Ngay khi bạn lấp đầy tiền trong phòng, căn phòng đã được lấp đầy.
Khi bạn đi đến phòng lớn hơn, giả sử nó chỉ có thể chứa thêm một tiền trong đó, tổng cộng là 2 lần đếm tiền. Điều đó có nghĩa là, tiền chuyển trước và thêm 1. Và một lần nữa nó được lấp đầy.
Bằng cách này, N đang tăng chậm và không còn O (1) nữa, vì chúng tôi đang chuyển tất cả tiền từ phòng trước, nhưng chỉ có thể chứa thêm 1 tiền nữa.
Sau 100 lần, phòng mới phù hợp với 100 số tiền từ trước đó và thêm 1 tiền nữa. Đây là O (N), vì O (N + 1) là O (N), nghĩa là mức 100 hoặc 101 là như nhau, cả hai đều là hàng trăm, trái ngược với câu chuyện trước đây, từ hàng triệu đến hàng tỷ .
Vì vậy, đây là cách phân bổ phòng (hoặc bộ nhớ / RAM) không hiệu quả cho tiền của chúng tôi (các biến).
Vì vậy, một cách tốt là phân bổ nhiều phòng hơn, với sức mạnh là 2.
Kích thước phòng 1 = vừa vặn 1 đếm tiền
Kích thước phòng 2 = vừa vặn 4 đếm tiền
Kích thước phòng 3 = vừa vặn 8 đếm tiền
Kích thước phòng 4 = vừa vặn 16 đếm tiền
Kích thước phòng 5 = vừa 32 đếm tiền
Kích thước phòng 6 = vừa 64 số tiền
Kích thước phòng thứ 7 = phù hợp với 128 số tiền
Kích thước phòng thứ 8 = phù hợp với 256 số tiền
Kích thước phòng thứ 9 = phù hợp với 512 số tiền
Kích thước phòng thứ 10 = phù hợp với 1024 số tiền
Kích thước phòng thứ 11 = phù hợp với 2.048 số tiền
. ..
Kích thước phòng thứ 16 = phù hợp với 65.536 số tiền
...
Kích thước phòng thứ 32 = phù hợp với 4.294.967.296 số tiền
...
Kích thước phòng thứ 64 = phù hợp với 18.446.744.073.709.551.616 số tiền
Tại sao điều này tốt hơn? Bởi vì nó có vẻ phát triển chậm khi bắt đầu và nhanh hơn về sau, nghĩa là, so với dung lượng bộ nhớ trong RAM của chúng tôi.
Điều này rất hữu ích vì, trong trường hợp đầu tiên mặc dù nó tốt, tổng số lượng công việc phải làm trên mỗi tiền là cố định (2) và không thể so sánh với kích thước phòng (N), phòng mà chúng tôi đã thực hiện trong giai đoạn ban đầu có thể quá lớn (1 triệu) mà chúng tôi có thể không sử dụng đầy đủ tùy thuộc vào việc chúng tôi có thể nhận được số tiền đó để tiết kiệm trong trường hợp đầu tiên hay không.
Tuy nhiên, trong trường hợp cuối cùng, sức mạnh của 2, nó phát triển trong giới hạn của RAM của chúng tôi. Và do đó, tăng sức mạnh lên 2, cả phân tích Armotized vẫn không đổi và nó rất thân thiện với RAM hạn chế mà chúng ta có cho đến ngày hôm nay.
Các giải thích ở trên áp dụng cho Phân tích Tổng hợp, ý tưởng lấy "trung bình" trên nhiều hoạt động. Tôi không chắc chắn làm thế nào họ áp dụng cho phương pháp Ngân hàng hoặc Phương pháp phân tích khấu hao vật lý.
Hiện nay. Tôi không chắc chắn chính xác của câu trả lời chính xác. Nhưng nó sẽ phải làm với điều kiện nguyên tắc của cả hai phương pháp Vật lý + Ngân hàng:
(Tổng chi phí khấu hao của hoạt động)> = (Tổng chi phí thực tế của hoạt động).
Khó khăn chính mà tôi gặp phải là do chi phí hoạt động của Amortized-tiệm cận khác với chi phí không triệu chứng-bình thường, tôi không chắc cách đánh giá mức độ quan trọng của chi phí khấu hao.
Đó là khi ai đó đưa ra chi phí khấu hao của tôi, tôi biết nó không giống như chi phí không triệu chứng thông thường Tôi rút ra kết luận gì từ chi phí khấu hao?
Vì chúng ta có trường hợp một số hoạt động bị tính phí quá mức trong khi các hoạt động khác bị tính phí thấp, một giả thuyết có thể là trích dẫn chi phí khấu hao của các hoạt động riêng lẻ sẽ là vô nghĩa.
Ví dụ: Đối với một heap của Wikipedia, trích dẫn chi phí khấu hao của việc chỉ giảm-Key là O (1) là vô nghĩa vì chi phí được giảm bằng "công việc được thực hiện bởi các hoạt động trước đó trong việc tăng tiềm năng của heap."
HOẶC LÀ
Chúng ta có thể có một giả thuyết khác về lý do khấu hao - chi phí như sau:
Tôi biết rằng hoạt động đắt tiền sẽ đi trước các hoạt động NHIỀU CHI PHÍ THẤP.
Để phân tích, tôi sẽ áp dụng một số hoạt động với chi phí thấp, NHƯ THẾ NÀO LÀ CHI PHÍ HẤP DẪN CỦA CHÚNG TÔI KHÔNG THAY ĐỔI.
Với các hoạt động chi phí thấp tăng lên này, tôi có thể CHỨNG MINH R OPNG HOẠT ĐỘNG MỞ RỘNG có CHI PHÍ NHỎ NHỎ.
Do đó, tôi đã cải thiện / giảm ASYMPTOTIC-BOUND về chi phí của n hoạt động.
Do đó, phân tích chi phí khấu hao + phân bổ chi phí phân bổ hiện chỉ áp dụng cho các hoạt động đắt tiền. Các hoạt động giá rẻ có cùng chi phí tiệm cận-khấu hao-chi phí như chi phí bình thường-tiệm cận-chi phí.
Hiệu suất của bất kỳ chức năng nào có thể được tính trung bình bằng cách chia "tổng số lần gọi hàm" thành "tổng thời gian thực hiện cho tất cả các cuộc gọi được thực hiện". Ngay cả các chức năng mất nhiều thời gian hơn và lâu hơn cho mỗi cuộc gọi bạn thực hiện có thể được tính trung bình theo cách này.
Vì vậy, bản chất của một chức năng thực hiện Constant Amortized Time
là "thời gian trung bình" này đạt đến mức trần không bị vượt quá khi số lượng cuộc gọi tiếp tục được tăng lên. Bất kỳ cuộc gọi cụ thể nào cũng có thể khác nhau về hiệu suất, nhưng trong thời gian dài, thời gian trung bình này sẽ không tăng lên ngày càng lớn hơn.
Đây là công đức thiết yếu của một cái gì đó thực hiện tại Constant Amortized Time
.