อัลกอริธึมแบบแทนที่สำหรับการแทรกอาร์เรย์


62

คุณได้รับองค์ประกอบมากมาย2n

a1,a2,,an,b1,b2,bn

ภารกิจคือการสอดแทรกอาร์เรย์โดยใช้อัลกอริทึมแบบแทนที่ที่อาร์เรย์ผลลัพธ์จะมีลักษณะดังนี้

b1,a1,b2,a2,,bn,an

หากความต้องการในสถานที่ไม่มีเราสามารถสร้างอาร์เรย์และองค์ประกอบการคัดลอกใหม่ที่ให้อัลกอริทึมเวลาO(n)

ด้วยความต้องการในสถานที่แบ่งและพิชิตอัลกอริทึมกระแทกขึ้นขั้นตอนวิธีการที่จะเป็นlog)θ(nlogn)

ดังนั้นคำถามคือ:

มีอัลกอริธึมเวลาซึ่งอยู่ในสถานที่ด้วยหรือไม่?O(n)

(หมายเหตุ: คุณสามารถสันนิษฐานได้ว่ารูปแบบ WORD RAM ราคาเท่ากันดังนั้นจึงแปลเป็นจำกัด พื้นที่)O(1)


1
นี่เป็นstackoverflow แต่ไม่ได้ให้โซลูชันที่มีคุณภาพ คำตอบที่ได้รับความนิยมสูงสุดคือ: "ปัญหานี้ไม่สำคัญเหมือนที่คนทั่วไปทำกันการบ้าน? LOL มีวิธีแก้ปัญหาเกี่ยวกับ arXiv " แต่วิธีแก้ปัญหา arxiv นั้นต้องใช้ทฤษฎีจำนวน + หลักฐานอ้างอิงในเอกสารอื่น ๆ มันคงจะดีถ้ามีทางออกสั้น ๆ อยู่ที่นี่
Joe

1
ยังอยู่ใน cstheory: cstheory.stackexchange.com/questions/13943/ …
Yuval Filmus

อีกหัวข้อใน Stack Overflow: stackoverflow.com/questions/15996288/…
Nayuki

คำตอบ:


43

นี่คือคำตอบที่ elaborates ตามอัลกอริทึมจากกระดาษที่เชื่อมโยงโดย Joe: http://arxiv.org/abs/0805.1598

ก่อนอื่นให้เราพิจารณาอัลกอริทึมซึ่งใช้การหารและพิชิตΘ(nlogn)

1) หารและพิชิต

เราจะได้รับ

a1,a2,,b1,b2,bn

ตอนนี้ใช้การหารและพิชิตสำหรับบางเราพยายามหาอาร์เรย์m=Θ(n)

[a1,a2,,am,b1,b2,,bm],[am+1,,an,bm+1,bn]

และการชดเชย

ขอให้สังเกตว่าส่วนเป็นการเปลี่ยนแปลงแบบวงกลมของ

b1,b2,bm,am+1,an

am+1,an,b1,bm

ตามสถานที่m

นี่เป็นแบบคลาสสิกและสามารถทำได้ภายในสามครั้งและเวลาO(n)

ดังนั้นการแบ่งและพิชิตช่วยให้คุณอัลกอริทึมที่มีการเรียกซ้ำคล้ายกับ(N)Θ(nlogn)T(n)=2T(n/2)+Θ(n)

2) วงจรการเปลี่ยนรูป

ทีนี้อีกแนวทางหนึ่งของปัญหาคือการพิจารณาการเปลี่ยนแปลงเป็นชุดของวงจรที่แยกจากกัน

การเปลี่ยนแปลงได้รับจาก (สมมติว่าเริ่มต้นที่ )1

j2jmod2n+1

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

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

นี่คือที่กระดาษใช้ทฤษฎีจำนวน

มันสามารถแสดงให้เห็นว่าในกรณีที่เมื่อองค์ประกอบที่ตำแหน่ง ,อยู่ในรอบที่แตกต่างกันและทุกรอบมีองค์ประกอบ ที่ตำแหน่ง02n+1=3k13,32,,3k13m,m0

นี้จะใช้ความจริงที่ว่าเป็นเครื่องกำเนิดไฟฟ้าของ *2(Z/3k)

ดังนั้นเมื่อวิธีการติดตามวัฏจักรทำให้เรามีอัลกอริธึมเวลาสำหรับแต่ละรอบเรารู้ว่าจะเริ่มต้นตรงไหน: พลังของ (รวม ) สามารถคำนวณในช่องว่าง )2n+1=3kO(n)31O(1)

3) อัลกอริทึมสุดท้าย

ตอนนี้เรารวมสองสิ่งข้างต้น: หารและพิชิต + เปลี่ยนวงจร

เราทำหารและพิชิต แต่รับเพื่อให้เป็นอำนาจของและ(N)m2m+13m=Θ(n)

ดังนั้นแทนที่จะเรียกซ้ำทั้งสอง "แบ่งเท่า ๆ กัน" เราจะจ่ายคืนเพียงครั้งเดียวและทำงานพิเศษΘ(n)

สิ่งนี้ทำให้เราเกิดซ้ำ (สำหรับ ) และทำให้เรามีเวลาอัลกอริทึมพื้นที่!T(n)=T(cn)+Θ(n)0<c<1O(n)O(1)


4
มันสวยงาม.
Raphael

1
ดีมาก. จากตัวอย่างการเรียงสับเปลี่ยนตอนนี้ฉันเข้าใจส่วนใหญ่แล้ว สองคำถาม: 1. คุณจะหาค่า m ได้อย่างไร? Paper อ้างว่าใช้ O (log n) ทำไม? 2. DE-interleave อาร์เรย์สามารถใช้วิธีการที่คล้ายกันได้หรือไม่?
num3ric

2
@ num3ric: 1) คุณพบว่าอำนาจสูงสุดของซึ่งเป็นn ดังนั้นมันจะเป็นn) 2) ใช่เป็นไปได้ฉันเชื่อว่าฉันได้เพิ่มคำตอบใน stackoverflow บางแห่ง ผู้นำวงในกรณีนั้นฉันเชื่อว่าจะเป็นของ (สำหรับ = พลังของ ) 3<nO(logn)2a3b2m+13
Aryabhata

@Aryabhata เหตุใดเราจึงเรียกเก็บเงินเพียงครึ่งเดียว "แทนที่จะเป็น" ครึ่งหนึ่ง "
sinoTrinity

1
@Aryabhata อัลกอริทึมนี้สามารถขยายเพื่อแทรกมากกว่าสองอาร์เรย์ได้หรือไม่? ตัวอย่างเช่นเปลี่ยนเป็นหรือสิ่งที่คล้ายกัน a1,a2,,an,b1,b2,,bn,c1,c2,,cnc1,b1,a1,c2,b2,a2,,cn,bn,an
Doub

18

ฉันค่อนข้างมั่นใจว่าฉันพบอัลกอริทึมที่ไม่พึ่งพาทฤษฎีจำนวนหรือทฤษฎีวัฏจักร โปรดทราบว่ามีรายละเอียดเล็กน้อยสำหรับการทำงาน (อาจเป็นในวันพรุ่งนี้) แต่ฉันค่อนข้างมั่นใจว่าพวกเขาจะได้ผล ฉันส่งด้วยมืออย่างที่ฉันควรจะนอนไม่ใช่เพราะฉันพยายามซ่อนปัญหา :)

อนุญาตAเป็นอาร์เรย์แรก, Bวินาที|A| = |B| = NและสมมติN=2^kสำหรับบางk, เพื่อความง่าย. อนุญาตA[i..j]เป็น subarray ของAกับดัชนีiผ่านเพื่อjรวม อาร์เรย์เป็นแบบ 0 ให้RightmostBitPos(i)ส่งคืนตำแหน่ง (อิงตาม 0) ของบิตขวาสุดที่ '1' iโดยนับจากทางขวา อัลกอริทึมทำงานดังนี้

GetIndex(i) {
    int rightPos = RightmostBitPos(i) + 1;
    return i >> rightPos;
}

Interleave(A, B, N) {
    if (n == 1) {
        swap(a[0], b[0]);
    }
    else {
        for (i = 0; i < N; i++)
            swap(A[i], B[GetIndex(i+1)]);

        for (i = 1; i <= N/2; i*=2)
            Interleave(B[0..i/2-1], B[i/2..i-1], i/2);

        Interleave(B[0..N/2], B[N/2+1..N], n/2);
    }
}

ลองหาตัวเลข 16 ตัวและลองเริ่มการแทรกโดยใช้การสลับและดูว่าเกิดอะไรขึ้น:

1 2 3 4 5 6 7 8    | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8    | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8    | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8   | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8   | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8  | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8  | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16

สิ่งที่น่าสนใจเป็นพิเศษคือส่วนแรกของอาร์เรย์ที่สอง:

|
| 1
| 2
| 2 3
| 4 3
| 4 3 5
| 4 6 5
| 4 6 5 7
| 8 6 5 7

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

ตอนนี้เราไปเพื่อเป็นตัวอย่างที่ใหญ่กว่าเพื่อดูว่าเราสามารถเห็นรูปแบบ โปรดทราบว่าเราไม่จำเป็นต้องแก้ไขขนาดของอาร์เรย์เพื่อสร้างตัวอย่างข้างต้น ในบางจุดเราจะได้รับการกำหนดค่านี้ (บรรทัดที่สองลบ 16 จากทุกหมายเลข):

16 24 20 28 18 22 26 30 17 19 21 23 25 27 29 31
0   8  4 12  2  6 10 14  1  3  5  7  9 11 13 15

ตอนนี้สิ่งนี้แสดงให้เห็นถึงรูปแบบที่ชัดเจน: "1 3 5 7 9 11 13 15" แยกออกจากกัน 2, "2 6 10 14" แยก 4 และ "4 12" แยกออกจากกัน 8 ดังนั้นเราสามารถกำหนดอัลกอริทึมที่บอกเราว่าจำนวนที่น้อยที่สุดถัดไปจะเป็นอย่างไร: กลไกนั้นเป็นวิธีการทำงานของเลขฐานสอง คุณมีบิตสำหรับช่วงครึ่งหลังของอาร์เรย์บิตสำหรับไตรมาสที่สองและอื่น ๆ

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

เราสามารถรับครึ่งแรกของอาร์เรย์เข้าสู่สถานะ interleaved ในเวลาและ swaps อย่างไรก็ตามเราต้องแก้ไขช่วงครึ่งหลังของอาเรย์ของเราซึ่งดูเหมือนจะสับสน ("8 6 5 7 13 14 15 16")O ( n )O(n)O(n)

ทีนี้ถ้าเราสามารถ 'จัดเรียง' ครึ่งแรกของส่วนที่สองนี้ได้เราจะจบด้วย "5 6 7 8 13 14 15 16" และการแทรกซ้ำครึ่งนี้จะทำเคล็ดลับ: เราสอดแทรกอาร์เรย์ใน time (เรียกการเรียกซ้ำแต่ละครั้งซึ่งขนาดอินพุตจะลดลงครึ่งหนึ่ง) หมายเหตุเราไม่จำเป็นต้องสแต็คเป็นสายเหล่านี้มีหาง recursive ดังนั้นการใช้พื้นที่ของเรายังคง(1)O ( บันทึกn ) O ( 1 )O(n)O(logn)O(1)

ตอนนี้คำถามคือ: มีรูปแบบบางอย่างในส่วนที่เราต้องเรียงลำดับ? การลองหมายเลข 32 ให้เรา "16 12 10 14 9 11 13 15" เพื่อแก้ไข โปรดทราบว่าเรามีรูปแบบเดียวกันแน่นอนที่นี่! "9 11 13 15", "10 14" และ "12" ถูกจัดกลุ่มเข้าด้วยกันในแบบเดียวกับที่เราเห็นก่อนหน้านี้

ทีนี้เคล็ดลับคือการสอดแทรกส่วนย่อยเหล่านี้ซ้ำ ๆ เราแทรก "16" และ "12" เป็น "12 16" เราแทรก "12 16" และ "10 14" ถึง "10 12 14 16" เรา interleave "10 12 14 16" และ "9 11 13 15" ถึง "9 10 11 12 13 14 15 16" นี่เป็นส่วนแรก

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

ตัวอย่าง:

Interleave the first half:
1 2 3 4 5 6 7 8    | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8    | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8    | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8   | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8   | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8  | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8  | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16
Sort out the first part of the second array (recursion not explicit):
8 6 5 7 13 14 15 16
6 8 5 7 13 14 15 16
5 8 6 7 13 14 15 16
5 6 8 7 13 14 15 16
5 6 7 8 13 14 15 16
Interleave again:
5 6 7 8   | 13 14 15 16
13 6 7 8  | 5 14 15 16
13 5 7 8  | 6 14 15 16
13 5 14 8 | 6 7 15 16
13 5 14 6 | 8 7 15 16
Sort out the first part of the second array:
8 7 15 16
7 8 15 16
Interleave again:
7 8 | 15 16
15 8 | 7 16
15 7 | 8 16
Interleave again:
8 16
16 8
Merge all the above:
9 1 10 2 11 3 12 4 | 13 5 14 6 | 15 7 | 16 8

น่าสนใจ คุณยินดีที่จะลองและเขียนหลักฐานเป็นทางการหรือไม่? ฉันรู้ว่ามีอัลกอริทึมอื่น (อ้างถึงในกระดาษโจพบ) ซึ่งจะจัดการกับบิต บางทีคุณอาจค้นพบมัน!
Aryabhata

1

นี่เป็นอัลกอริธึมแบบไม่เรียกซ้ำในเวลาเชิงเส้นเพื่อแทรกสองครึ่งของอาร์เรย์โดยไม่มีหน่วยเก็บพิเศษ

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

เราเริ่มต้นด้วยอาร์เรย์ขนาด N ที่แบ่งออกเป็น 2 ส่วนเท่ากันเกือบเท่ากัน
[ left_items | right_items ]
เมื่อเราประมวลผลมันจะกลายเป็น
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]

พื้นที่สว็อปจะเพิ่มขึ้นด้วยรูปแบบต่อไปนี้: ก) ขยายพื้นที่โดยการลบรายการที่อยู่ติดกันและสลับเป็นรายการใหม่จากทางซ้าย B) สลับรายการที่เก่าแก่ที่สุดด้วยรายการใหม่จากด้านซ้าย หากรายการด้านซ้ายมีหมายเลข 1..N รูปแบบนี้จะดูเหมือน

step swapspace index changed
1    A: 1         0
2    B: 2         0
3    A: 2 3       1
4    B: 4 3       0     
5    A: 4 3 5     2
6    B: 4 6 5     1
7    A: 4 6 5 7   3
...

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

นั่นคือข้อมูลทั้งหมดที่เราต้องเติมในครึ่งแรกของลำดับในเวลาเชิงเส้น

เมื่อเราไปถึงจุดกึ่งกลางอาร์เรย์จะมีสามส่วน: [ placed_items | swapped_left_items | remaining_right_items] หากเราสามารถถอดรหัสรายการที่สลับได้เราจะลดปัญหาดังกล่าวเป็นครึ่งหนึ่งของขนาดและสามารถทำซ้ำได้

เพื่อถอดรหัสพื้นที่ swap เราจะใช้คุณสมบัติดังต่อไปนี้: ลำดับที่สร้างขึ้นโดยNสลับผนวกและการดำเนินงาน swap_oldest จะมีรายการที่ทุกเพศทุกวัยของพวกเขาจะได้รับจาก N/2 (การหารจำนวนเต็มค่าที่น้อยกว่าจะเก่ากว่า)A025480(N/2)..A025480(N-1)

ตัวอย่างเช่นถ้าช่วงครึ่งซ้ายเดิมจัดขึ้นค่า 1..19 แล้วพื้นที่ swap [16, 12, 10, 14, 18, 11, 13, 15, 17, 19]จะประกอบด้วย A025480 (9..18) คือ[2, 5, 1, 6, 3, 7, 0, 8, 4, 9]ซึ่งเป็นรายการดัชนีของรายการจากเก่าไปหาใหม่

ดังนั้นเราจึงสามารถถอดรหัสพื้นที่ swap ของเราโดย advancing ผ่านมันและการแลกเปลี่ยนกับS[i] S[ A(N/2 + i)]นี่คือเวลาเชิงเส้น

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

ณ จุดนี้เราได้รวมอาร์เรย์ครึ่งหนึ่งและรักษาลำดับของส่วนที่ไม่ได้ถูกแยกในอีกครึ่งหนึ่งด้วยการN/2 + N/4แลกเปลี่ยนที่แน่นอน เราสามารถดำเนินการต่อผ่านส่วนที่เหลือของอาร์เรย์รวมของสัญญาซึ่งเป็นอย่างเคร่งครัดน้อยกว่าN + N/4 + N/8 + .... 3N/2

วิธีการคำนวณ A025480:
นี้ถูกกำหนดไว้ใน OEIS เป็นสูตรอื่นคือa(2n) = n, a(2n+1) = a(n). a(n) = isEven(n)? n/2 : a((n-1)/2)สิ่งนี้นำไปสู่อัลกอริทึมอย่างง่ายโดยใช้การดำเนินการระดับบิต:

index_t a025480(index_t n){
    while (n&1) n=n>>1;
    return n>>1;  
}

นี่คือ O ตัดจำหน่าย (1) การดำเนินงานมากกว่าค่าที่เป็นไปได้ทั้งหมดสำหรับ N. (1/2 จำเป็น 1 กะ 1/4 จำเป็น 2 1/8 จำเป็น 3, ... ) มีวิธีที่เร็วกว่าซึ่งใช้ตารางการค้นหาขนาดเล็กเพื่อค้นหาตำแหน่งบิตที่มีนัยสำคัญน้อยที่สุด

ระบุว่านี่คือการดำเนินการใน C:

static inline index_t larger_half(index_t sz) {return sz - (sz / 2); }
static inline bool is_even(index_t i) { return ((i & 1) ^ 1); }

index_t unshuffle_item(index_t j, index_t sz)
{
  index_t i = j;
  do {
    i = a025480(sz / 2 + i);
  }
  while (i < j);
  return i;
}

void interleave(value_t a[], index_t n_items)
{
  index_t i = 0;
  index_t midpt = larger_half(n_items);
  while (i < n_items - 1) {

    //for out-shuffle, the left item is at an even index
    if (is_even(i)) { i++; }
    index_t base = i;

    //emplace left half.
    for (; i < midpt; i++) {
      index_t j = a025480(i - base);
      SWAP(a + i, a + midpt + j);
    }

    //unscramble swapped items
    index_t swap_ct  = larger_half(i - base);
    for (index_t j = 0; j + 1 < swap_ct ; j++) {
      index_t k = unshuffle_item(j, i - base);
      if (j != k) {
        SWAP(a + midpt + j, a + midpt + k);
      }
    }
    midpt += swap_ct;
  }
}

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

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