Tại sao std :: list :: Reverse có độ phức tạp O (n)?


192

Tại sao hàm đảo ngược cho std::listlớp trong thư viện chuẩn C ++ có thời gian chạy tuyến tính? Tôi nghĩ rằng đối với các danh sách liên kết đôi, hàm đảo ngược phải là O (1).

Đảo ngược một danh sách liên kết đôi chỉ nên liên quan đến việc chuyển đổi đầu và con trỏ đuôi.


18
Tôi không hiểu tại sao mọi người lại hạ thấp câu hỏi này. Đó là một câu hỏi hoàn toàn hợp lý để hỏi. Việc đảo ngược danh sách liên kết đôi sẽ mất thời gian O (1).
Tò mò

46
Thật không may, một số người nhầm lẫn giữa các khái niệm "câu hỏi là tốt" với "câu hỏi có một ý tưởng tốt". Tôi thích những câu hỏi như thế này, về cơ bản "sự hiểu biết của tôi có vẻ khác với một thực tiễn thường được chấp nhận, xin vui lòng giúp giải quyết mâu thuẫn này", bởi vì việc mở rộng cách bạn nghĩ giúp bạn giải quyết nhiều vấn đề hơn! Nó sẽ xuất hiện những người khác sử dụng cách tiếp cận "đó là một sự lãng phí khi xử lý trong 99.9999% trường hợp, thậm chí không nghĩ về nó". Nếu đó là bất kỳ sự an ủi nào, tôi đã bị hạ thấp xuống rất nhiều, ít hơn nhiều!
corsiKa

4
Vâng câu hỏi này có một số lượng downvote không phù hợp cho chất lượng của nó. Có lẽ nó giống như người đã nâng cao câu trả lời của Blindy. Nói một cách công bằng, "đảo ngược danh sách liên kết đôi chỉ nên liên quan đến việc chuyển đổi con trỏ đầu và đuôi" nói chung là không đúng với danh sách liên kết tiêu chuẩn ngụ ý rằng mọi người đều học ở trường trung học hoặc cho nhiều triển khai mà mọi người sử dụng. Rất nhiều lần trong phản ứng ruột của SO đối với câu hỏi hoặc câu trả lời thúc đẩy quyết định upvote / downvote. Nếu bạn đã rõ ràng hơn trong câu đó hoặc bỏ qua nó, tôi nghĩ rằng bạn sẽ nhận được ít downvote hơn.
Chris Beck

3
Hoặc để tôi đặt gánh nặng chứng minh với bạn, @Cpered: Tôi đã đưa ra một triển khai danh sách liên kết gấp đôi ở đây: ideone.com/c1HebO . Bạn có thể chỉ ra cách bạn mong đợi Reversehàm được thực hiện trong O (1) không?
CompuChip

2
@CompuChip: Trên thực tế, tùy thuộc vào việc thực hiện, nó có thể không. Bạn không cần thêm boolean để biết nên sử dụng con trỏ nào: chỉ cần sử dụng con trỏ không quay lại với bạn ... điều này cũng có thể tự động với danh sách được liên kết của XOR. Vì vậy, có, nó phụ thuộc vào cách danh sách được thực hiện và tuyên bố OP có thể được làm rõ.
Matthieu M.

Câu trả lời:


187

Theo giả thuyết, reversecó thể là O (1) . Ở đó (một lần nữa theo giả thuyết) có thể là một thành viên danh sách boolean cho biết hướng của danh sách được liên kết hiện tại giống hay ngược với hướng ban đầu nơi danh sách được tạo.

Thật không may, điều đó sẽ làm giảm hiệu suất của bất kỳ hoạt động nào khác (mặc dù không thay đổi thời gian chạy tiệm cận). Trong mỗi hoạt động, một boolean sẽ cần được tư vấn để xem xét nên theo con trỏ "tiếp theo" hay "trước" của một liên kết.

Vì đây có lẽ được coi là một hoạt động tương đối không thường xuyên, nên tiêu chuẩn (không quy định việc triển khai, chỉ phức tạp), quy định rằng độ phức tạp có thể là tuyến tính. Điều này cho phép các con trỏ "tiếp theo" luôn có nghĩa là cùng một hướng rõ ràng, tăng tốc các hoạt động trong trường hợp thông thường.


29
@MooseBoys: Tôi không đồng ý với sự tương tự của bạn. Sự khác biệt là, trong trường hợp của một danh sách, việc thực hiện có thể cung cấp reversevới O(1)độ phức tạp mà không ảnh hưởng lớn-o của bất kỳ hoạt động khác , bằng cách sử dụng lá cờ lừa boolean này. Nhưng, trong thực tế, một nhánh phụ trong mọi hoạt động là tốn kém, ngay cả khi về mặt kỹ thuật là O (1). Ngược lại, bạn không thể tạo cấu trúc danh sách trong đó sortlà O (1) và tất cả các hoạt động khác đều có cùng chi phí. Vấn đề của câu hỏi là, dường như, bạn có thể O(1)đảo ngược miễn phí nếu bạn chỉ quan tâm đến chữ O lớn, vậy tại sao họ không làm điều đó.
Chris Beck

9
Nếu bạn đã sử dụng danh sách liên kết XOR, việc đảo ngược sẽ trở thành thời gian không đổi. Một iterator sẽ lớn hơn mặc dù, và tăng / giảm nó sẽ đắt hơn một chút về mặt tính toán. Điều đó có thể bị lấn át bởi các truy cập bộ nhớ không thể tránh khỏi mặc dù đối với bất kỳ loại danh sách liên kết nào.
Ded repeatator

3
@IlyaPopov: Có phải mọi nút thực sự cần điều này? Người dùng không bao giờ hỏi bất kỳ câu hỏi nào của nút danh sách, chỉ có thân danh sách chính. Vì vậy, truy cập boolean là dễ dàng cho bất kỳ phương thức mà người dùng gọi. Bạn có thể đưa ra quy tắc rằng các trình vòng lặp bị vô hiệu nếu danh sách bị đảo ngược và / hoặc lưu trữ một bản sao của boolean với trình vòng lặp, chẳng hạn. Vì vậy, tôi nghĩ rằng nó sẽ không ảnh hưởng đến chữ O lớn. Tôi sẽ thừa nhận, tôi đã không đi từng dòng thông số kỹ thuật. :)
Chris Beck

4
@Kevin: Hừm? Dù sao bạn cũng không thể xor hai con trỏ trực tiếp, trước tiên bạn cần chuyển đổi chúng thành số nguyên (rõ ràng là loại std::uintptr_t. Sau đó, bạn có thể xor chúng.
Ded repeatator

3
@Kevin, bạn chắc chắn có thể tạo một danh sách liên kết XOR trong C ++, thực tế đó là ngôn ngữ áp phích cho loại điều này. Không có gì nói bạn phải sử dụng std::uintptr_t, bạn có thể chuyển sang một charmảng và sau đó XOR các thành phần. Nó sẽ chậm hơn nhưng di động 100%. Có khả năng bạn có thể làm cho nó chọn giữa hai triển khai này và chỉ sử dụng lần thứ hai làm dự phòng nếu uintptr_tbị thiếu. Một số nếu nó được mô tả trong câu trả lời này: stackoverflow.com/questions/14243971/ Kẻ
Chris Beck

61

có thểO (1) nếu danh sách sẽ lưu trữ một lá cờ cho phép hoán đổi ý nghĩa của các con trỏ của nhà cái prevnextmột trong những nút có. Nếu việc đảo ngược danh sách sẽ là một hoạt động thường xuyên, thì việc bổ sung như vậy trên thực tế có thể hữu ích và tôi không biết bất kỳ lý do nào khiến việc thực hiện nó sẽ bị cấm theo tiêu chuẩn hiện tại. Tuy nhiên, việc có một lá cờ như vậy sẽ khiến cho việc duyệt qua danh sách trở nên đắt đỏ hơn (nếu chỉ bởi một yếu tố không đổi) bởi vì thay vì

current = current->next;

trong operator++danh sách lặp, bạn sẽ nhận được

if (reversed)
  current = current->prev;
else
  current = current->next;

đó không phải là thứ bạn quyết định thêm dễ dàng. Cho rằng các danh sách thường được duyệt qua thường xuyên hơn nhiều so với chúng bị đảo ngược, sẽ rất không khôn ngoan khi tiêu chuẩn bắt buộc kỹ thuật này. Do đó, hoạt động ngược lại được phép có độ phức tạp tuyến tính. Tuy nhiên, xin lưu ý rằng tO (1) tO ( n ) vì vậy, như đã đề cập trước đó, việc triển khai tối ưu hóa kỹ thuật của bạn sẽ được cho phép.

Nếu bạn đến từ Java hoặc nền tương tự, bạn có thể tự hỏi tại sao trình lặp phải kiểm tra cờ mỗi lần. Thay vào đó, chúng ta không thể có hai loại trình lặp khác nhau, cả hai đều xuất phát từ một loại cơ sở chung, và có std::list::beginstd::list::rbegintrả về đa hình cho trình lặp thích hợp không? Trong khi có thể, điều này sẽ làm cho toàn bộ điều thậm chí còn tồi tệ hơn bởi vì tiến bộ iterator sẽ là một cuộc gọi hàm gián tiếp (khó để nội tuyến) ngay bây giờ. Trong Java, dù sao bạn cũng trả giá này thường xuyên, nhưng một lần nữa, đây là một trong những lý do khiến nhiều người tiếp cận với C ++ khi hiệu suất rất quan trọng.

Như được chỉ ra bởi Benjamin Lindley trong các bình luận, vì reversekhông được phép vô hiệu hóa các trình vòng lặp, cách tiếp cận duy nhất được tiêu chuẩn cho phép dường như là lưu trữ một con trỏ trở lại danh sách bên trong iterator gây ra truy cập bộ nhớ gián tiếp kép.


7
@galinette: std::list::reversekhông làm mất hiệu lực các trình vòng lặp.
Benjamin Lindley

1
@galinette Xin lỗi, tôi đã đọc nhầm nhận xét trước đó của bạn là cờ trên mỗi iterator, trái ngược với cờ của cờ trên mỗi nút, khi bạn viết nó. Tất nhiên, một cờ trên mỗi nút sẽ phản tác dụng như bạn, một lần nữa, phải thực hiện một giao dịch tuyến tính để lật tất cả chúng.
5gon12eder

2
@ 5gon12eder: Bạn có thể loại bỏ việc phân nhánh với chi phí rất mất: lưu trữ nextprevcon trỏ trong một mảng và lưu trữ hướng dưới dạng 0hoặc 1. Để lặp lại phía trước, bạn sẽ theo dõi pointers[direction]và lặp lại theo chiều ngược lại pointers[1-direction](hoặc ngược lại). Điều này vẫn sẽ thêm một chút chi phí nhỏ, nhưng có lẽ ít hơn một nhánh.
Jerry Coffin

4
Bạn có thể không thể lưu trữ một con trỏ vào danh sách bên trong các trình vòng lặp. swap()được chỉ định là thời gian không đổi và không làm mất hiệu lực bất kỳ trình vòng lặp nào.
Tavian Barnes

1
@TavianBarnes Chết tiệt! Chà, ba lần chuyển hướng sau đó có thể (ý tôi là, không thực sự là ba. Bạn sẽ phải lưu trữ cờ trong một đối tượng được phân bổ động nhưng con trỏ trong trình lặp tất nhiên có thể trỏ trực tiếp vào đối tượng đó thay vì gián tiếp qua danh sách.)
5gon12eder

37

Chắc chắn vì tất cả các container hỗ trợ các trình vòng lặp hai chiều đều có khái niệm rbegin () và render (), câu hỏi này có phải không?

Việc xây dựng một proxy đảo ngược các trình vòng lặp và truy cập vào container thông qua đó là chuyện nhỏ.

Sự không hoạt động này thực sự là O (1).

nhu la:

#include <iostream>
#include <list>
#include <string>
#include <iterator>

template<class Container>
struct reverse_proxy
{
    reverse_proxy(Container& c)
    : _c(c)
    {}

    auto begin() { return std::make_reverse_iterator(std::end(_c)); }
    auto end() { return std::make_reverse_iterator(std::begin(_c)); }

    auto begin() const { return std::make_reverse_iterator(std::end(_c)); }
    auto end() const { return std::make_reverse_iterator(std::begin(_c)); }

    Container& _c;
};

template<class Container>
auto reversed(Container& c)
{
    return reverse_proxy<Container>(c);
}

int main()
{
    using namespace std;
    list<string> l { "the", "cat", "sat", "on", "the", "mat" };

    auto r = reversed(l);
    copy(begin(r), end(r), ostream_iterator<string>(cout, "\n"));

    return 0;
}

sản lượng dự kiến:

mat
the
on
sat
cat
the

Với điều này, dường như với tôi, ủy ban tiêu chuẩn đã không mất thời gian để bắt buộc O (1) sắp xếp ngược lại container vì không cần thiết, và thư viện tiêu chuẩn được xây dựng chủ yếu theo nguyên tắc chỉ bắt buộc những gì thực sự cần thiết trong khi tránh trùng lặp.

Chỉ cần 2c của tôi.


18

Bởi vì nó phải đi qua mọi nút ( ntổng cộng) và cập nhật dữ liệu của họ (thực sự là bước cập nhật O(1)). Điều này làm cho toàn bộ hoạt động O(n*1) = O(n).


27
Bởi vì bạn cũng cần cập nhật các liên kết giữa mỗi mục. Lấy ra một mảnh giấy và rút nó ra thay vì bỏ phiếu.
Bịt mắt

11
Tại sao hỏi nếu bạn rất chắc chắn sau đó? Bạn đang lãng phí cả thời gian của chúng tôi.
Bịt mắt

28
Các nút của danh sách được liên kết đôi có ý nghĩa về hướng. Lý do từ đó.
Giày

11
@Blindy Một câu trả lời tốt nên được hoàn thành. "Lấy ra một mảnh giấy và rút nó ra" do đó không phải là một thành phần cần thiết của một câu trả lời hay. Câu trả lời không phải là câu trả lời tốt có thể bị downvote.
RM

7
@Shoe: Họ có phải không? Vui lòng nghiên cứu danh sách liên kết XOR và tương tự.
Ded repeatator

2

Nó cũng hoán đổi con trỏ trước đó và tiếp theo cho mọi nút. Đó là lý do tại sao nó cần tuyến tính. Mặc dù có thể được thực hiện trong O (1) nếu hàm sử dụng LL này cũng lấy thông tin về LL làm đầu vào như liệu nó đang truy cập bình thường hay đảo ngược.


1

Chỉ có một lời giải thích thuật toán. Hãy tưởng tượng bạn có một mảng với các phần tử, sau đó bạn cần đảo ngược nó. Ý tưởng cơ bản là lặp lại trên từng phần tử thay đổi phần tử ở vị trí đầu tiên sang vị trí cuối cùng, phần tử ở vị trí thứ hai sang vị trí áp chót, v.v. Khi bạn đạt đến giữa mảng, bạn sẽ có tất cả các phần tử thay đổi, do đó, trong (n / 2) lần lặp, được coi là O (n).


1

Đó là O (n) đơn giản vì nó cần sao chép danh sách theo thứ tự ngược lại. Mỗi thao tác mục riêng lẻ là O (1) nhưng có n trong số chúng trong toàn bộ danh sách.

Tất nhiên, có một số hoạt động liên tục trong thời gian liên quan đến việc thiết lập không gian cho danh sách mới và thay đổi con trỏ sau đó, v.v. Ký hiệu O không xem xét các hằng số riêng lẻ khi bạn đưa vào hệ số n bậc nhất.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.