ทำไม std :: list :: reverse มีความซับซ้อน O (n)


192

เหตุใดฟังก์ชันย้อนกลับสำหรับstd::listคลาสในไลบรารีมาตรฐาน C ++ จึงมี Linear runtime ฉันคิดว่าสำหรับรายการที่ลิงก์สองเท่าฟังก์ชันย้อนกลับควรเป็น O (1)

การย้อนกลับรายการที่เชื่อมโยงเป็นทวีคูณควรเกี่ยวข้องกับการสลับหัวและตัวชี้ท้าย


18
ฉันไม่เข้าใจว่าทำไมผู้คนถึงลงคะแนนคำถามนี้ เป็นคำถามที่เหมาะสมอย่างยิ่งที่จะถาม การย้อนกลับรายการที่เชื่อมโยงเป็นสองเท่าควรใช้เวลา O (1)
อยากรู้อยากเห็น

46
น่าเสียดายที่บางคนสับสนแนวคิดของ "คำถามที่ดี" กับ "คำถามมีความคิดที่ดี" ฉันรักคำถามเช่นนี้โดยที่ "ความเข้าใจของฉันดูเหมือนแตกต่างจากการปฏิบัติที่ยอมรับกันโดยทั่วไปโปรดช่วยแก้ไขข้อขัดแย้งนี้" เนื่องจากการขยายวิธีที่คุณคิดว่าช่วยให้คุณแก้ปัญหาได้มากขึ้น! มันจะปรากฏขึ้นที่คนอื่นใช้วิธีการของ "ที่เสียการประมวลผลในกรณี 99.9999% ไม่ต้องคิดเกี่ยวกับมัน" ถ้ามันเป็นความปลอบใจใด ๆ ฉันได้รับการโหวตให้ลดน้อยลงมาก!
corsiKa

4
ใช่คำถามนี้มีจำนวน downvote มากเกินไปสำหรับคุณภาพของมัน อาจเป็นเช่นเดียวกับผู้ที่ตอบคำถามของ Blindy ในความเป็นธรรม "การย้อนกลับรายการที่เชื่อมโยงเป็นทวีคูณควรเกี่ยวข้องกับการสลับตัวชี้หัวและหาง" โดยทั่วไปแล้วไม่เป็นความจริงสำหรับรายการเชื่อมโยงมาตรฐานหมายความว่าทุกคนเรียนรู้ในโรงเรียนมัธยมหรือสำหรับการใช้งานหลายอย่าง หลายครั้งในปฏิกิริยาทางเดินอาหารของผู้คน SO ทันทีต่อคำถามหรือคำตอบทำให้เกิดการตัดสินใจในการโหวตขึ้น / ลง หากคุณมีความชัดเจนมากขึ้นในประโยคนั้นหรือละเว้นมันฉันคิดว่าคุณจะได้รับ downvote น้อยลง
Chris Beck

3
หรือให้ฉันใส่ภาระการพิสูจน์กับคุณ @Curious: ผมวิปปิ้งขึ้นการดำเนินรายการที่เชื่อมโยงเป็นทวีคูณที่นี่: ideone.com/c1HebO คุณสามารถระบุได้อย่างไรว่าคุณคาดหวังว่าReverseฟังก์ชั่นนี้จะใช้งานใน O (1)
CompuChip

2
@CompuChip: จริง ๆ แล้วมันอาจไม่ได้ คุณไม่จำเป็นต้องใช้บูลีนพิเศษในการรู้ว่าตัวชี้ใดที่จะใช้: เพียงแค่ใช้ตัวชี้ที่ไม่ได้ชี้ไปที่คุณ ... ซึ่งอาจเป็นไปโดยอัตโนมัติด้วยรายการที่ลิงก์ของ XOR'ed ดังนั้นใช่มันขึ้นอยู่กับวิธีการใช้รายการและคำสั่ง OP สามารถชี้แจงได้
Matthieu M.

คำตอบ:


187

สมมุติฐานreverseที่จะได้รับO (1) ที่นั่น (สมมุติฐานอีกครั้ง) อาจเป็นสมาชิกรายการบูลีนที่ระบุว่าทิศทางของรายการที่เชื่อมโยงอยู่ในขณะนี้เหมือนกันหรือตรงข้ามกับต้นฉบับที่รายการถูกสร้างขึ้น

น่าเสียดายที่จะลดประสิทธิภาพของการดำเนินการอื่น ๆ เป็นหลัก (แม้ว่าจะไม่ได้เปลี่ยนรันไทม์แบบอะซิมโทติก) ในแต่ละการดำเนินการบูลีนจะต้องได้รับการพิจารณาเพื่อพิจารณาว่าจะทำตามตัวชี้ "ถัดไป" หรือ "ก่อนหน้า" ของลิงก์

ตั้งแต่นี้ถือว่าสันนิษฐานว่าเป็นการดำเนินการที่ค่อนข้างบ่อยมาตรฐาน (ซึ่งไม่ได้กำหนดใช้งานความซับซ้อนเท่านั้น) ระบุว่าความซับซ้อนอาจเป็นเชิงเส้น สิ่งนี้จะช่วยให้พอยน์เตอร์ "ถัดไป" หมายถึงทิศทางเดียวกันเสมอไปอย่างไม่น่าเชื่อทำให้การทำงานของเคสธรรมดาเป็นไปอย่างรวดเร็วขึ้น


29
@MooseBoys: ฉันไม่เห็นด้วยกับการเปรียบเทียบของคุณ ความแตกต่างคือว่าในกรณีของรายการที่ดำเนินการจะให้reverseมีO(1)ความซับซ้อนโดยไม่มีผลกระทบใหญ่ o ของการดำเนินการอื่น ๆโดยใช้เคล็ดลับนี้ธงบูลีน แต่ในทางปฏิบัติสาขาพิเศษในทุกการปฏิบัติการมีค่าใช้จ่ายสูงแม้ว่าจะเป็นเทคนิค O (1) ในทางตรงกันข้ามคุณไม่สามารถสร้างโครงสร้างรายการที่sortเป็น O (1) และการดำเนินการอื่น ๆ ทั้งหมดมีค่าใช้จ่ายเท่ากัน ประเด็นของคำถามคือดูเหมือนว่าคุณสามารถO(1)ย้อนกลับได้ฟรีถ้าคุณสนใจเพียงแค่ O ใหญ่เท่านั้นทำไมพวกเขาถึงไม่ทำอย่างนั้น
Chris Beck

9
หากคุณใช้ XOR-linked-list การย้อนกลับจะกลายเป็นเวลาคงที่ ตัววนซ้ำจะใหญ่กว่าและการเพิ่ม / ลดลงจะมีราคาแพงกว่าเล็กน้อย ที่อาจถูกแคระโดยการเข้าถึงหน่วยความจำที่หลีกเลี่ยงไม่ได้ แต่สำหรับรายการที่เชื่อมโยงทุกชนิด
Deduplicator

3
@IlyaPopov: ทุกโหนดต้องการสิ่งนี้จริงหรือ ผู้ใช้ไม่เคยถามคำถามใด ๆ เกี่ยวกับโหนดรายการตัวเองเพียงรายการเนื้อหาหลัก ดังนั้นการเข้าถึงบูลีนจึงเป็นเรื่องง่ายสำหรับวิธีการใด ๆ ที่ผู้ใช้เรียก คุณสามารถกำหนดกฎที่ตัววนซ้ำไม่ถูกต้องหากรายการถูกย้อนกลับและ / หรือเก็บสำเนาบูลีนด้วยตัววนซ้ำตัวอย่างเช่น ดังนั้นฉันคิดว่ามันจะไม่ส่งผลกระทบต่อ O ขนาดใหญ่อย่างแน่นอน ฉันจะยอมรับว่าฉันไม่ได้ไปทีละบรรทัดผ่านข้อกำหนด :)
Chris Beck

4
@ เควิน: หืมอะไรนะ? คุณไม่สามารถ xor สองตัวชี้โดยตรงอยู่แล้วคุณจะต้องแปลงให้เป็นจำนวนเต็มแรก (ชัดจากประเภทstd::uintptr_t. จากนั้นคุณสามารถแฮคเกอร์พวกเขา.
Deduplicator

3
@Kevin คุณสามารถสร้างรายชื่อ XOR ที่เชื่อมโยงใน C ++ ได้จริง ๆ แล้วมันเป็นภาษาเด็กเล็กสำหรับสิ่งนี้ ไม่มีอะไรบอกว่าคุณต้องใช้std::uintptr_tคุณสามารถส่งไปยังcharอาร์เรย์แล้ว XOR ส่วนประกอบ มันจะช้ากว่า แต่พกพาได้ 100% ดูเหมือนว่าคุณสามารถเลือกระหว่างการนำไปใช้งานทั้งสองนี้และใช้ที่สองเป็นทางเลือกหากuintptr_tขาดหายไป บางอย่างหากมีการอธิบายไว้ในคำตอบนี้: stackoverflow.com/questions/14243971/…
Chris Beck

61

มันอาจจะเป็นO (1) ถ้ารายการจะเก็บธงที่ช่วยให้การแลกเปลี่ยนความหมายของ“A prev” และ“ next” ชี้แต่ละโหนดมี หากการย้อนกลับรายการเป็นการดำเนินการบ่อยครั้งการเพิ่มอาจเป็นประโยชน์จริง ๆ และฉันไม่รู้ว่าทำไมการใช้งานนั้นจะถูกห้ามโดยมาตรฐานปัจจุบัน อย่างไรก็ตามการมีค่าสถานะดังกล่าวจะทำให้การสำรวจเส้นทางปกติของรายการมีราคาแพงกว่า (หากมีเพียงปัจจัยคงที่) เพราะแทนที่จะเป็น

current = current->next;

ในoperator++รายการตัววนซ้ำคุณจะได้รับ

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

ซึ่งไม่ใช่สิ่งที่คุณต้องการเพิ่มอย่างง่ายดาย ระบุว่ารายการที่มักจะมีการสำรวจมากบ่อยกว่าที่พวกเขาจะกลับก็จะไม่ฉลาดมากสำหรับมาตรฐานเพื่ออาณัติเทคนิคนี้ ดังนั้นการดำเนินการย้อนกลับได้รับอนุญาตให้มีความซับซ้อนเชิงเส้น อย่างไรก็ตามโปรดทราบว่าt permitted O (1) ⇒ tO ( n ) ดังนั้นตามที่กล่าวไว้ก่อนหน้านี้การใช้“ การเพิ่มประสิทธิภาพ” ของคุณจะได้รับอนุญาตทางเทคนิค

หากคุณมาจาก Java หรือพื้นหลังที่คล้ายกันคุณอาจสงสัยว่าทำไมตัววนซ้ำต้องตรวจสอบการตั้งค่าสถานะในแต่ละครั้ง เราไม่สามารถมีตัววนซ้ำที่แตกต่างกันสองประเภท แต่ทั้งสองมาจากประเภทพื้นฐานทั่วไปและมีstd::list::beginและstd::list::rbeginส่งคืนตัววนซ้ำที่เหมาะสมแบบ polymorphically ในขณะที่เป็นไปได้สิ่งนี้จะทำให้ทุกอย่างเลวร้ายยิ่งขึ้นเนื่องจากการเลื่อนตัววนซ้ำจะเป็นการเรียกใช้ฟังก์ชันทางอ้อม ใน Java คุณจะจ่ายราคานี้เป็นประจำอยู่แล้ว แต่อีกครั้งนี่เป็นหนึ่งในเหตุผลที่หลาย ๆ คนเข้าถึง C ++ เมื่อประสิทธิภาพมีความสำคัญ

ดังที่เบนจามินลินด์ลีย์ชี้ให้เห็นในความคิดเห็นเนื่องจากreverseไม่ได้รับอนุญาตให้ทำให้ตัววนซ้ำใช้ไม่ได้แนวทางเดียวที่ได้รับอนุญาตตามมาตรฐานดูเหมือนจะเก็บตัวชี้กลับไปยังรายการภายในตัววนซ้ำซึ่งทำให้หน่วยความจำเข้าถึง


7
@galinette: std::list::reverseไม่ทำให้เป็นตัววนซ้ำ
Benjamin Lindley

1
@galinette ขออภัยฉันอ่านความคิดเห็นก่อนหน้านี้ของคุณว่า "ตั้งค่าสถานะสำหรับตัววนซ้ำ " ซึ่งตรงข้ามกับ "ตั้งค่าสถานะต่อโหนด " ตามที่คุณเขียน แน่นอนว่าการตั้งค่าสถานะต่อโหนดนั้นจะเป็นไปในทางตรงกันข้ามเช่นเดียวกับที่คุณต้องทำการสำรวจเชิงเส้นเพื่อพลิกมันทั้งหมด
5gon12eder

2
@ 5gon12eder: คุณสามารถขจัดความแตกแขนงที่ค่าใช้จ่ายที่หายไปมาก: เก็บnextและprevตัวชี้ในอาร์เรย์และเก็บทิศทางที่เป็นหรือ0 1หากต้องการทำซ้ำไปข้างหน้าคุณจะต้องติดตามpointers[direction]และทำซ้ำในสิ่งที่ตรงกันข้ามpointers[1-direction](หรือกลับกัน) สิ่งนี้จะยังคงเพิ่มค่าใช้จ่ายเล็กน้อย แต่อาจน้อยกว่าสาขา
Jerry Coffin

4
คุณอาจไม่สามารถเก็บตัวชี้ไปยังรายการภายในตัววนซ้ำ swap()ถูกระบุว่าเป็นเวลาคงที่และไม่ทำให้ตัววนซ้ำใด ๆ ใช้งานไม่ได้
Tavian Barnes

1
@TavianBarnes ประณามมัน! ดีร้ายสามแล้ว ... (ผมหมายถึงไม่จริงสามคุณจะต้องเก็บธงในวัตถุจัดสรร แต่ชี้ใน iterator สามารถของจุดแน่นอนโดยตรงกับวัตถุแทน indirecting กว่ารายการที่..)
5gon12eder

37

แน่นอนเนื่องจากคอนเทนเนอร์ทั้งหมดที่สนับสนุนตัววนซ้ำแบบสองทิศทางมีแนวคิดของ rbegin () และ rend () คำถามนี้คือ moot หรือไม่?

มันไม่สำคัญเลยที่จะสร้างพร็อกซีที่ย้อนกลับตัววนซ้ำและเข้าถึงคอนเทนเนอร์ผ่านทางนั้น

การไม่ทำงานนี้เป็น O (1)

เช่น:

#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;
}

ผลลัพธ์ที่คาดหวัง:

mat
the
on
sat
cat
the

ให้ฉันดูเหมือนว่าคณะกรรมการมาตรฐานไม่ได้ใช้เวลาในการสั่ง O (1) ย้อนกลับการสั่งซื้อของภาชนะเพราะมันไม่จำเป็นและห้องสมุดมาตรฐานส่วนใหญ่สร้างขึ้นบนหลักการของการมอบสิ่งที่จำเป็นเท่านั้นในขณะที่ หลีกเลี่ยงการทำซ้ำ

แค่ 2c ของฉัน


18

เพราะมันต้องผ่านทุกโหนด ( nทั้งหมด) และอัปเดตข้อมูลของพวกเขา (ขั้นตอนการอัพเดทแน่นอนO(1)) O(n*1) = O(n)นี้จะทำให้การดำเนินการทั้งหมด


27
เพราะคุณต้องอัปเดตลิงก์ระหว่างทุกรายการด้วย นำกระดาษออกมาแล้ววาดออกมาแทนการลดลง
Blindy

11
ทำไมถามว่าคุณแน่ใจเหรอ? คุณกำลังสูญเสียทั้งเวลาของเรา
Blindy

28
@CULL Doubly ที่เชื่อมโยงรายการของโหนดมีความรู้สึกถึงทิศทาง เหตุผลจากที่นั่น
รองเท้า

11
@Blindy คำตอบที่ดีควรจะสมบูรณ์ "หยิบกระดาษหนึ่งแผ่นออกมาแล้วดึงออกมา" ดังนั้นจึงไม่ควรเป็นองค์ประกอบที่จำเป็นของคำตอบที่ดี คำตอบที่ไม่ใช่คำตอบที่ดีนั้นขึ้นอยู่กับ downvotes
RM

7
@Shoe: พวกเขาต้องทำยังไง? กรุณาวิจัย XOR-linked-list และรายการที่คล้ายกัน
Deduplicator

2

นอกจากนี้ยังสลับตัวชี้ก่อนหน้าและถัดไปสำหรับทุกโหนด นั่นเป็นเหตุผลว่าทำไมมันต้องใช้ Linear แม้ว่าจะสามารถทำได้ใน O (1) ถ้าฟังก์ชั่นที่ใช้ LL นี้ยังใช้ข้อมูลเกี่ยวกับ LL เป็นอินพุตเช่นไม่ว่าจะเป็นการเข้าถึงตามปกติหรือย้อนกลับ


1

คำอธิบายอัลกอริทึมเท่านั้น ลองนึกภาพคุณมีอาร์เรย์ที่มีองค์ประกอบจากนั้นคุณจะต้องกลับรายการ แนวคิดพื้นฐานคือการทำซ้ำในแต่ละองค์ประกอบการเปลี่ยนองค์ประกอบในตำแหน่งแรกไปยังตำแหน่งสุดท้ายองค์ประกอบในตำแหน่งที่สองไปยังตำแหน่งสุดท้ายและอื่น ๆ เมื่อคุณไปถึงจุดกึ่งกลางของอาร์เรย์คุณจะมีการเปลี่ยนแปลงองค์ประกอบทั้งหมดดังนั้นใน (n / 2) การวนซ้ำซึ่งถือว่าเป็น O (n)


1

เป็น O (n) เพียงเพราะต้องการคัดลอกรายการในลำดับย้อนกลับ การดำเนินการแต่ละรายการเป็น O (1) แต่มี n รายการในรายการทั้งหมด

แน่นอนว่ามีการดำเนินการเวลาคงที่ที่เกี่ยวข้องในการตั้งค่าพื้นที่สำหรับรายการใหม่และการเปลี่ยนพอยน์เตอร์ในภายหลังเป็นต้นสัญกรณ์ O จะไม่พิจารณาค่าคงที่แต่ละรายการเมื่อคุณรวมปัจจัย n ลำดับแรก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.