การใช้กองซ้อนโดยใช้สองคิวคืออะไร


34

ฉันมีคำถามทำการบ้านต่อไปนี้:

ใช้วิธีการซ้อน (x) และ pop () โดยใช้สองคิว

ดูเหมือนว่าจะแปลกสำหรับฉันเพราะ:

  • Stack คือคิว (LIFO)
  • ฉันไม่เห็นว่าทำไมคุณต้องใช้สองคิวในการติดตั้ง

ฉันค้นหารอบ ๆ :

และพบวิธีแก้ไขปัญหาสองสามข้อ นี่คือสิ่งที่ฉันลงเอยด้วย:

public class Stack<T> {
    LinkedList<T> q1 = new LinkedList<T>();
    LinkedList<T> q2 = new LinkedList<T>();

    public void push(T t) {
        q1.addFirst(t);
    }

    public T pop() {
        if (q1.isEmpty()) {
            throw new RuntimeException(
                "Can't pop from an empty stack!");
        }

        while(q1.size() > 1) {
            q2.addFirst( q1.removeLast() );
        }

        T popped = q1.pop();

        LinkedList<T> tempQ = q1;
        q1 = q2;
        q2 = tempQ;

        return popped;
    }
}

แต่ฉันไม่เข้าใจว่าข้อดีคืออะไรมากกว่าการใช้คิวเดียว รุ่นคิวสองดูเหมือนซับซ้อนอย่างไม่มีจุดหมาย

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

ฉันพลาดอะไรไปรึเปล่า? ความเข้าใจใด ๆ ที่นี่จะได้รับการชื่นชม


17
โดยทั่วไปแล้วคิวจะอ้างถึงโครงสร้าง FIFO ในขณะที่สแต็กเป็นโครงสร้าง LIFO อินเตอร์เฟสสำหรับ LinkedList ใน Java คือ deque (คิวสิ้นสุดวันที่สองครั้ง) ซึ่งอนุญาตให้เข้าถึงทั้ง FIFO และ LIFO ลองเปลี่ยนการเขียนโปรแกรมเป็นอินเตอร์เฟสคิวแทนการใช้งาน LinkedList

12
ปัญหาที่ปกติมากขึ้นคือการใช้คิวโดยใช้สองกอง คุณอาจพบว่าหนังสือของ Chris Okasaki เกี่ยวกับโครงสร้างข้อมูลที่น่าสนใจ
Eric Lippert

2
สร้างสิ่งที่ Eric กล่าวว่าบางครั้งคุณอาจพบว่าตัวเองเป็นภาษาสแต็ก (เช่นdcหรือกดลงอัตโนมัติด้วยสองกอง (เทียบเท่ากับเครื่องจักรทัวริงเพราะดีคุณสามารถทำมากขึ้น)) หลายกอง แต่ไม่มีคิว

1
@MichaelT: หรือคุณยังสามารถพบว่าตัวเองทำงานอยู่บนกองตาม CPU
slebetman

11
"สแต็กคือคิว (LIFO)" ... uhm คิวคือคิวรอ ชอบบรรทัดสำหรับการใช้ห้องน้ำสาธารณะ บรรทัดที่คุณรออยู่นั้นเคยทำงานในรูปแบบ LIFO หรือไม่? หยุดใช้คำว่า "คิว LIFO" มันไร้สาระ
Mehrdad

คำตอบ:


44

ไม่มีข้อได้เปรียบ: นี่เป็นแบบฝึกหัดทางวิชาการอย่างแท้จริง

มากนานมาแล้วตอนที่ผมยังเป็นนักศึกษาใหม่ในวิทยาลัยผมมีการออกกำลังกายที่คล้ายกัน1 เป้าหมายคือเพื่อสอนนักเรียนถึงวิธีการใช้การเขียนโปรแกรมเชิงวัตถุเพื่อใช้อัลกอริทึมแทนการเขียนวิธีแก้ปัญหาซ้ำโดยใช้forลูปกับตัวนับลูป แต่ให้รวมและนำโครงสร้างข้อมูลที่มีอยู่กลับมาใช้ใหม่เพื่อให้บรรลุเป้าหมายของคุณ

คุณจะไม่ใช้รหัสนี้ในโลกแห่งความจริงTM สิ่งที่คุณต้องนำออกไปจากแบบฝึกหัดนี้คือวิธี "คิดนอกกรอบ" และใช้รหัสซ้ำ


โปรดทราบว่าคุณควรใช้อินเตอร์เฟสjava.util.Queueในรหัสของคุณแทนที่จะใช้งานโดยตรง:

Queue<T> q1 = new LinkedList<T>();
Queue<T> q2 = new LinkedList<T>();

วิธีนี้ช่วยให้คุณสามารถใช้การใช้งานอื่น ๆ ได้Queueหากต้องการรวมถึงวิธีการซ่อน2วิธีLinkedListที่อาจเป็นไปได้ที่จิตวิญญาณของQueueอินเทอร์เฟซ ซึ่งรวมถึงget(int)และpop()(ในขณะที่รหัสของคุณรวบรวมมีข้อผิดพลาดทางตรรกะในนั้นให้ข้อ จำกัด ของการมอบหมายของคุณประกาศตัวแปรของคุณQueueแทนLinkedListจะเปิดเผยมัน) การอ่านที่เกี่ยวข้อง: การทำความเข้าใจ“ การเขียนโปรแกรมสู่อินเทอร์เฟซ”และเพราะเหตุใดอินเทอร์เฟซจึงมีประโยชน์

1 ฉันยังจำได้: แบบฝึกหัดคือการย้อนกลับสแต็กโดยใช้วิธีการเฉพาะในส่วนต่อประสานสแต็กและไม่มีวิธีการยูทิลิตี้ในjava.util.Collectionsหรือคลาสยูทิลิตี้ "คงที่เท่านั้น" วิธีการแก้ไขที่ถูกต้องเกี่ยวข้องกับการใช้โครงสร้างข้อมูลอื่นเป็นวัตถุการพักชั่วคราว: คุณต้องรู้โครงสร้างข้อมูลที่แตกต่างกันคุณสมบัติของพวกเขาและวิธีรวมเข้าด้วยกันเพื่อทำมัน นิ่งงันในชั้นเรียน CS101 ส่วนใหญ่ของฉันซึ่งไม่เคยตั้งโปรแกรมมาก่อน

2 วิธีการยังคงอยู่ที่นั่น แต่คุณไม่สามารถเข้าถึงได้โดยไม่ต้องใช้ casts หรือการสะท้อนกลับ ดังนั้นจึงไม่ใช่เรื่องง่ายที่จะใช้วิธีที่ไม่ใช่คิวเหล่านั้น


1
ขอบคุณ ฉันเดาว่ามันสมเหตุสมผล ฉันยังตระหนักว่าฉันกำลังใช้การดำเนินการ "ผิดกฎหมาย" ในรหัสด้านบน (ผลักไปที่ด้านหน้าของ FIFO) แต่ฉันไม่คิดว่ามันจะเปลี่ยนแปลงอะไร ฉันกลับการดำเนินการทั้งหมดและยังคงทำงานตามที่ตั้งใจไว้ ฉันจะรอสักครู่ก่อนที่จะยอมรับเพราะฉันไม่ต้องการกีดกันผู้อื่นจากการให้ข้อมูล ขอบคุณค่ะ
Carcigenicate

19

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

แต่มันเป็นไปได้ คุณสามารถใช้สิ่งที่เป็นนามธรรมเพื่อนำไปใช้กับสิ่งอื่นและในทางกลับกัน สแต็กสามารถนำมาใช้ในแง่ของสองคิวและในทำนองเดียวกันคุณสามารถใช้คิวในแง่ของสองสแต็ค ข้อดีของแบบฝึกหัดนี้คือ:

  • คุณสรุปสแต็ค
  • คุณสรุปคิว
  • คุณคุ้นเคยกับการคิดอัลกอริทึม
  • คุณเรียนรู้อัลกอริทึมที่เกี่ยวข้องกับสแต็ก
  • คุณต้องคิดถึงการแลกเปลี่ยนในอัลกอริทึม
  • ด้วยการตระหนักถึงความเท่าเทียมกันของคิวและสแต็คคุณจะเชื่อมโยงหัวข้อต่าง ๆ ของหลักสูตรของคุณ
  • คุณจะได้รับประสบการณ์การเขียนโปรแกรมเชิงปฏิบัติ

จริงๆแล้วนี่เป็นการออกกำลังกายที่ยอดเยี่ยม ฉันควรทำเองตอนนี้ :)


3
@JohnKugelman ขอบคุณสำหรับการแก้ไขของคุณ แต่ฉันหมายถึง "ความซับซ้อนของเวลาที่น่ากลัว" จริงๆ สำหรับการเชื่อมโยงรายชื่อตามสแต็คpush, peekและpopการดำเนินงานใน O (1) เหมือนกันสำหรับสแต็กที่ยึดตามอาร์เรย์ที่ปรับขนาดได้ยกเว้นว่าpushเป็นค่า O (1) ที่ถูกตัดจำหน่ายด้วย O (n) กรณีที่เลวร้ายที่สุด เมื่อเปรียบเทียบกับแบบนั้นการเรียงซ้อนตามคิวนั้นด้อยกว่าอย่างมากด้วยการกด O (n), O (1) ป๊อปและแอบดูหรือผลักดัน O (1), ป๊อป O (n) และมอง
amon

1
"ความซับซ้อนของเวลาที่น่ากลัว" และ "ด้อยกว่าอย่างมากมาย" นั้นไม่ถูกต้อง ความซับซ้อนตัดจำหน่ายยังคงเป็น O (1) สำหรับการพุชและป๊อป มีคำถามที่สนุกสนานใน TAOCP (vol1?) เกี่ยวกับเรื่องนั้น (โดยทั่วไปคุณต้องแสดงให้เห็นว่าจำนวนครั้งที่องค์ประกอบสามารถสลับจากสแต็กหนึ่งไปยังอีกสแต็คเป็นค่าคงที่) ผลการดำเนินงานกรณีที่เลวร้ายที่สุดสำหรับการดำเนินการอย่างใดอย่างหนึ่งที่แตกต่างกัน แต่แล้วผมไม่ค่อยได้ยินใครพูดคุยเกี่ยวกับ O (n) ประสิทธิภาพการทำงานสำหรับการผลักดันใน ArrayLists - ไม่มักตัวเลขที่น่าสนใจ
Voo

5

มีวัตถุประสงค์ที่แท้จริงในการสร้างคิวจากสองกอง หากคุณใช้โครงสร้างข้อมูลที่ไม่เปลี่ยนรูปแบบจากภาษาที่ใช้งานได้คุณสามารถผลักดันเข้าไปในกลุ่มรายการที่สามารถผลักดันได้และดึงจากรายการของรายการที่ปรากฏได้ ไอเท็ม poppable จะถูกสร้างขึ้นเมื่อไอเท็มทั้งหมดถูกดึงออกมาและสแต็กที่ poppable ใหม่คือการย้อนกลับของสแต็กที่พุชได้ มันมีประสิทธิภาพ

สำหรับสแต็กที่ทำจากสองคิว? ซึ่งอาจสมเหตุสมผลในบริบทที่คุณมีคิวจำนวนมากและรวดเร็วที่พร้อมใช้งาน มันไร้ประโยชน์อย่างแน่นอนในการฝึกหัดจาวาแบบนี้ แต่มันอาจสมเหตุสมผลถ้าเป็นแชแนลหรือคิวการส่งข้อความ (เช่น: ไม่มีข้อความที่จัดคิวด้วยการดำเนินการ O (1) เพื่อย้ายรายการ (N-1) ที่ด้านหน้าไปยังคิวใหม่)


อืม .. สิ่งนี้ทำให้ฉันคิดว่าการใช้ shift register เป็นพื้นฐานของการคำนวณและเกี่ยวกับสถาปัตยกรรมของเข็มขัด / โรงสี
2015

ว้าว Mill CPU นั้นน่าสนใจจริงๆ "เครื่องคิว" แน่นอน
Rob

2

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


2

ดังที่คนอื่น ๆ ระบุไว้แล้ว: ไม่มีความได้เปรียบในโลกแห่งความเป็นจริง

อย่างไรก็ตามหนึ่งคำตอบในส่วนที่สองของคำถามของคุณทำไมไม่ใช้เพียงคิวเดียวก็เกิน Java

ใน Java Queueอินเตอร์เฟสยังมีsize()เมธอดและการใช้งานมาตรฐานทั้งหมดของเมธอดนั้นคือ O (1)

นั่นไม่จำเป็นต้องเป็นจริงสำหรับรายการที่เชื่อมโยงแบบไร้เดียงสา / canonical เนื่องจากโปรแกรมเมอร์ C / C ++ จะใช้งานซึ่งจะเก็บพอยน์เตอร์ไว้ที่องค์ประกอบแรกและสุดท้ายและแต่ละองค์ประกอบจะชี้ไปยังองค์ประกอบถัดไป

ในกรณีนี้size()คือ O (n) และควรหลีกเลี่ยงในลูป หรือการดำเนินการเป็นขาวขุ่นและมีขั้นต่ำเปลือยและadd()remove()

ด้วยการใช้งานดังกล่าวคุณจะต้องนับจำนวนองค์ประกอบก่อนโดยโอนไปยังคิวที่สองโอนn-1องค์ประกอบกลับไปที่คิวแรกและส่งคืนองค์ประกอบที่เหลือ

ที่กล่าวว่ามันอาจจะไม่ทำสิ่งนี้ถ้าคุณอาศัยอยู่ใน Java-land


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

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

0

มันยากที่จะจินตนาการถึงการใช้งานสำหรับการนำไปปฏิบัติเช่นนั้นจริง แต่ส่วนใหญ่ของจุดที่จะพิสูจน์ว่ามันสามารถทำได้

อย่างไรก็ตามในแง่ของการใช้งานจริงสำหรับสิ่งเหล่านี้ฉันคิดได้สองอย่าง สิ่งหนึ่งที่ใช้ในการนี้คือการนำระบบไปใช้ในสภาพแวดล้อมที่ จำกัด ซึ่งไม่ได้ออกแบบมาสำหรับมันตัวอย่างเช่นบล็อก Redstone ของ Minecraftเปิดออกเพื่อเป็นตัวแทนของระบบทัวริงที่สมบูรณ์ซึ่งผู้คนเคยใช้ในการใช้วงจรตรรกะและแม้แต่ CPU ทั้งหมด ในช่วงแรกของการเล่นเกมที่ใช้สคริปต์สคริปต์บอทเกมแรกจำนวนมากก็ถูกนำไปใช้เช่นกัน

แต่คุณยังสามารถนำหลักการนี้ไปใช้ในทางกลับกันเพื่อให้มั่นใจว่าบางสิ่งเป็นไปไม่ได้ในระบบเมื่อคุณไม่ต้องการให้เป็นเช่นนั้น สิ่งนี้สามารถเกิดขึ้นได้ในบริบทความปลอดภัย: ตัวอย่างเช่นระบบการกำหนดค่าที่มีประสิทธิภาพสามารถเป็นสินทรัพย์ได้ แต่ยังมีระดับพลังงานที่คุณอาจไม่ได้มอบให้แก่ผู้ใช้ นี่เป็นการ จำกัด สิ่งที่คุณสามารถอนุญาตให้ใช้ภาษาการกำหนดค่าเพื่อมิให้ผู้บุกรุกถูกโจมตี แต่ในกรณีนี้คือสิ่งที่คุณต้องการ

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