ใช้ Random และ Order เป็นอัลกอริธึมการสับเปลี่ยนที่ดีหรือไม่


164

ฉันได้อ่านบทความเกี่ยวกับขั้นตอนวิธีการสับเปลี่ยนต่างๆมากกว่าที่Coding สยองขวัญ ฉันได้เห็นว่ามีคนทำสิ่งนี้เพื่อสลับรายการ:

var r = new Random();
var shuffled = ordered.OrderBy(x => r.Next());

นี่เป็นอัลกอริทึมสลับที่ดีหรือไม่ มันทำงานอย่างไรกันแน่? มันเป็นวิธีที่ยอมรับได้ของการทำเช่นนี้?

คำตอบ:


205

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

ฉันชอบชุดสับเปลี่ยนแบบเปลี่ยนจากFisher-Yatesของ Durstenfield ซึ่งมีการสลับสับเปลี่ยนองค์ประกอบ

การใช้Shuffleวิธีการขยายอย่างง่ายโดยทั่วไปจะประกอบด้วยการโทรToListหรือToArrayป้อนข้อมูลจากนั้นใช้การดำเนินการที่มีอยู่ของ Fisher-Yates (ผ่านRandomเป็นพารามิเตอร์เพื่อให้ชีวิตโดยทั่วไปดีกว่า) มีการใช้งานมากมายรอบ ... ฉันอาจได้คำตอบในที่ใดที่หนึ่ง

สิ่งที่ดีเกี่ยวกับวิธีการขยายดังกล่าวก็คือมันจะมีความชัดเจนกับผู้อ่านว่าคุณกำลังพยายามทำอะไร

แก้ไข: นี่คือการใช้งานง่าย (ไม่มีการตรวจสอบข้อผิดพลาด!):

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
    T[] elements = source.ToArray();
    // Note i > 0 to avoid final pointless iteration
    for (int i = elements.Length-1; i > 0; i--)
    {
        // Swap element "i" with a random earlier element it (or itself)
        int swapIndex = rng.Next(i + 1);
        T tmp = elements[i];
        elements[i] = elements[swapIndex];
        elements[swapIndex] = tmp;
    }
    // Lazily yield (avoiding aliasing issues etc)
    foreach (T element in elements)
    {
        yield return element;
    }
}

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

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
    T[] elements = source.ToArray();
    for (int i = elements.Length - 1; i >= 0; i--)
    {
        // Swap element "i" with a random earlier element it (or itself)
        // ... except we don't really need to swap it fully, as we can
        // return it immediately, and afterwards it's irrelevant.
        int swapIndex = rng.Next(i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
    }
}

ตอนนี้จะทำงานเท่าที่จำเป็น

โปรดทราบว่าในทั้งสองกรณีคุณต้องระมัดระวังเกี่ยวกับอินสแตนซ์ที่Randomคุณใช้เป็น:

  • การสร้างสองอินสแตนซ์Randomที่ประมาณในเวลาเดียวกันจะให้ผลตามลำดับของตัวเลขสุ่ม (เมื่อใช้ในวิธีเดียวกัน)
  • Random ไม่ปลอดภัยสำหรับเธรด

ฉันมีบทความเกี่ยวกับRandomรายละเอียดเพิ่มเติมเกี่ยวกับปัญหาเหล่านี้และการแก้ปัญหา


5
การใช้งานขนาดเล็ก แต่สำคัญสิ่งนี้ฉันจะพูดว่าดีเสมอที่จะพบที่นี่ใน StackOverflow ดังนั้นใช่กรุณาถ้าคุณต้องการที่จะ =)
Svish

9
จอน - คำอธิบายของคุณเกี่ยวกับ Fisher-Yates นั้นเทียบเท่ากับการนำไปปฏิบัติในคำถาม (รุ่นไร้เดียงสา) Durstenfeld / Knuth บรรลุ O (n) ไม่ใช่โดยการมอบหมาย แต่โดยการเลือกจากชุดที่ลดลงและการแลกเปลี่ยน วิธีนี้จำนวนสุ่มที่เลือกอาจทำซ้ำและอัลกอริทึมใช้เวลาเพียง O (n)
tvanfosson

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

3
@tvanfosson: ไม่ป่วยเลย :) แต่ใช่ผู้โทรควรทราบว่ามันประเมินอย่างเกียจคร้าน
Jon Skeet

4
สายไปหน่อย แต่โปรดทราบsource.ToArray();ว่าคุณต้องมีusing System.Linq;ไฟล์เดียวกัน หากไม่เป็นเช่นนั้นคุณจะได้รับข้อผิดพลาดนี้:'System.Collections.Generic.IEnumerable<T>' does not contain a definition for 'ToArray' and no extension method 'ToArray' accepting a first argument of type 'System.Collections.Generic.IEnumerable<T>' could be found (are you missing a using directive or an assembly reference?)
Powerlord

70

นี้จะขึ้นอยู่กับจอนสกีตของคำตอบ

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

อัลกอริทึมนี้มีการใช้มากในเกมที่มีการเลือกสามรายการแรกและอื่น ๆ จะต้องใช้ในภายหลังถ้าเลย ข้อเสนอแนะของฉันคือyieldตัวเลขทันทีที่พวกเขาเปลี่ยน สิ่งนี้จะช่วยลดต้นทุนเริ่มต้นในขณะที่ยังคงต้นทุนการทำซ้ำที่ O (1) (โดยทั่วไปคือ 5 การดำเนินงานต่อการทำซ้ำ) ค่าใช้จ่ายทั้งหมดจะยังคงเท่าเดิม แต่การสับจะเร็วขึ้น ในกรณีที่สิ่งนี้ถูกเรียกว่าcollection.Shuffle().ToArray()มันจะไม่สร้างความแตกต่างในทางทฤษฎี แต่ในกรณีการใช้งานดังกล่าวจะช่วยเร่งความเร็วในการสตาร์ทเครื่อง นอกจากนี้สิ่งนี้จะทำให้อัลกอริทึมมีประโยชน์สำหรับกรณีที่คุณต้องการเพียงรายการที่ไม่ซ้ำกัน ตัวอย่างเช่นหากคุณต้องการดึงไพ่สามใบออกจากสำรับไพ่ 52 ใบคุณสามารถโทรออกได้deck.Shuffle().Take(3)และจะมีการแลกเปลี่ยนสามครั้งเท่านั้น (แม้ว่าจะต้องคัดลอกอาร์เรย์ทั้งหมดก่อน)

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
    T[] elements = source.ToArray();
    // Note i > 0 to avoid final pointless iteration
    for (int i = elements.Length - 1; i > 0; i--)
    {
        // Swap element "i" with a random earlier element it (or itself)
        int swapIndex = rng.Next(i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
        // we don't actually perform the swap, we can forget about the
        // swapped element because we already returned it.
    }

    // there is one item remaining that was not returned - we return it now
    yield return elements[0]; 
}

อุ๊ย! สิ่งนี้อาจจะไม่ส่งคืนรายการทั้งหมดในแหล่งที่มา คุณไม่สามารถพึ่งพาตัวเลขสุ่มที่ไม่ซ้ำกันสำหรับการทำซ้ำ N
P พ่อ

2
ฉลาด! (และฉันเกลียดสิ่งนี้ 15 ตัวอักษร ... )
Svish

@P Daddy: ใช่มั้ย สนใจที่จะทำอย่างละเอียด?
Svish

1
หรือคุณอาจจะแทนที่> 0 พร้อมด้วย> = 0 และไม่ต้อง (แม้ว่าตี RNG พิเศษบวกมอบหมายซ้ำซ้อน)
FryGuy

4
ค่าใช้จ่ายเริ่มต้นคือ O (N) เป็นค่าใช้จ่ายของแหล่งที่มา ToArray ();
Dave Hillier

8

เริ่มจากคำพูดของ Skeet นี้:

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

ฉันจะอธิบายเหตุผลเล็กน้อยเกี่ยวกับความหวังที่ไม่เหมือนใคร!

ตอนนี้จากEnumerable.OrderBy :

วิธีนี้ทำการเรียงลำดับที่เสถียร นั่นคือถ้าคีย์ของสององค์ประกอบเท่ากันลำดับขององค์ประกอบจะถูกเก็บรักษาไว้

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

ตอนนี้จริงหรือ มันจริงหรอ?

และเช่นเคยหากมีข้อสงสัยให้เขียนโปรแกรมบางบรรทัด: http://pastebin.com/5CDnUxPG

บล็อกเล็ก ๆ ของโค้ดนี้จะสับเปลี่ยนอาร์เรย์ 3 องค์ประกอบจำนวนครั้งโดยใช้อัลกอริทึม Fisher-Yates ทำย้อนหลังอัลกอริทึม Fisher-Yates ทำไปข้างหน้า (ในหน้าwikiมีอัลกอริธึมหลอกรหัสสอง ... ผลลัพธ์ แต่หนึ่งเสร็จจากองค์ประกอบแรกถึงสุดท้ายขณะที่อีกองค์ประกอบหนึ่งทำจากองค์ประกอบสุดท้ายถึงองค์ประกอบแรก) อัลกอริทึมที่ไร้เดียงสาของhttp://blog.codinghorror.com/the-danger-of-naivete/และใช้.OrderBy(x => r.Next())และ.OrderBy(x => r.Next(someValue))และ

ตอนนี้Random.Nextคือ

จำนวนเต็มที่มีลายเซ็น 32- บิตที่มากกว่าหรือเท่ากับ 0 และน้อยกว่า MaxValue

ดังนั้นมันจึงเทียบเท่า

OrderBy(x => r.Next(int.MaxValue))

เพื่อทดสอบว่ามีปัญหานี้หรือไม่เราสามารถขยายอาร์เรย์ (บางสิ่งช้ามาก) หรือลดค่าสูงสุดของตัวสร้างหมายเลขสุ่ม ( int.MaxValueไม่ใช่หมายเลข "พิเศษ" ... มันเป็นเพียงตัวเลขที่ใหญ่มาก) ในท้ายที่สุดถ้าอัลกอริทึมไม่ได้มีความเอนเอียงโดยความนิ่งของOrderByช่วงค่าใด ๆ ควรให้ผลลัพธ์เหมือนกัน

โปรแกรมจะทดสอบค่าบางค่าในช่วง 1 ... 4096 เมื่อดูที่ผลลัพธ์มันค่อนข้างชัดเจนว่าสำหรับค่าต่ำ (<128) อัลกอริทึมนั้นมีอคติมาก (4-8%) มี 3 r.Next(1024)ค่าที่คุณต้องไม่น้อยกว่า หากคุณทำให้อาร์เรย์มีขนาดใหญ่ขึ้น (4 หรือ 5) แสดงว่ายังr.Next(1024)ไม่เพียงพอ ฉันไม่ใช่ผู้เชี่ยวชาญในการสับและคณิตศาสตร์ แต่ฉันคิดว่าสำหรับความยาวพิเศษของแต่ละแถวคุณต้องมีค่าสูงสุดอีก 2 บิต (เนื่องจากวันเกิดเส้นขนานเชื่อมต่อกับ sqrt (ตัวเลข)) ดังนั้น ถ้าค่าสูงสุดคือ 2 ^ 31 ฉันจะบอกว่าคุณควรจะสามารถเรียงลำดับอาร์เรย์ได้มากถึง 2 ^ 12/2 ^ 13 บิต (องค์ประกอบ 4096-8192)


ระบุไว้อย่างดีและแสดงปัญหาได้อย่างสมบูรณ์แบบกับคำถามเดิม สิ่งนี้ควรรวมเข้ากับคำตอบของจอน
TheSoftwareJedi

6

มันอาจเป็นไปได้อย่างสมเหตุสมผลสำหรับวัตถุประสงค์ส่วนใหญ่และเกือบทุกครั้งที่มันสร้างการแจกแจงแบบสุ่มอย่างแท้จริง (ยกเว้นเมื่อ Random.Next () สร้างจำนวนเต็มแบบสุ่มที่เหมือนกันสองรายการ)

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

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


4

เรื่องนี้เกิดขึ้นหลายครั้งก่อน ค้นหา Fisher-Yates บน StackOverflow

นี่คือตัวอย่างโค้ด C # ที่ฉันเขียนสำหรับอัลกอริทึมนี้ คุณสามารถกำหนดค่าพารามิเตอร์เป็นประเภทอื่นได้หากต้องการ

static public class FisherYates
{
        //      Based on Java code from wikipedia:
        //      http://en.wikipedia.org/wiki/Fisher-Yates_shuffle
        static public void Shuffle(int[] deck)
        {
                Random r = new Random();
                for (int n = deck.Length - 1; n > 0; --n)
                {
                        int k = r.Next(n+1);
                        int temp = deck[n];
                        deck[n] = deck[k];
                        deck[k] = temp;
                }
        }
}

2
คุณไม่ควรใช้Randomเป็นตัวแปรคงที่เช่นนี้ - Randomไม่ปลอดภัยสำหรับเธรด ดูcsharpindepth.com/Articles/Chapter12/Random.aspx
Jon Skeet

@ จอน Skeet: ใช่นั่นเป็นเหตุผลที่ถูกต้อง OTOH, OP กำลังถามเกี่ยวกับอัลกอริทึมที่ผิดพลาดในขณะที่สิ่งนี้ถูกต้อง (นอกเหนือจากกรณีการใช้การ์ดสับแบบหลายเธรด)
hughdbrown

1
นั่นหมายถึงว่านี่คือ "ผิดน้อย" กว่าวิธีของ OP ไม่ได้หมายความว่าเป็นรหัสที่ควรใช้โดยไม่เข้าใจว่าไม่สามารถใช้อย่างปลอดภัยในบริบทแบบมัลติเธรด ... ซึ่งเป็นสิ่งที่คุณไม่ได้กล่าวถึง มีความคาดหวังที่สมเหตุสมผลว่าสมาชิกแบบสแตติกสามารถใช้อย่างปลอดภัยจากหลายเธรด
Jon Skeet

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

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

3

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

ทางเลือกหนึ่งที่เป็นไปได้คือการให้ seed ส่งผ่านเป็นพารามิเตอร์ไปยังตัวสร้างหมายเลขสุ่ม (หรือตัวสร้างแบบสุ่มเป็นพารามิเตอร์) ดังนั้นคุณสามารถควบคุมได้มากขึ้นและทดสอบได้ง่ายขึ้น


3

ฉันพบว่าคำตอบของ Jon Skeet เป็นที่น่าพอใจอย่างสิ้นเชิง แต่ robo-scanner ของลูกค้าของฉันจะรายงานRandomว่ามีข้อบกพร่องด้านความปลอดภัย System.Security.Cryptography.RNGCryptoServiceProviderดังนั้นผมจึงสลับออกสำหรับ เป็นโบนัสมันแก้ไขปัญหาความปลอดภัยของเธรดที่กล่าวถึง ในทางกลับกันRNGCryptoServiceProviderวัดได้ช้ากว่าการใช้ 300xRandomช้ากว่าการใช้

การใช้งาน:

using (var rng = new RNGCryptoServiceProvider())
{
    var data = new byte[4];
    yourCollection = yourCollection.Shuffle(rng, data);
}

วิธี:

/// <summary>
/// Shuffles the elements of a sequence randomly.
/// </summary>
/// <param name="source">A sequence of values to shuffle.</param>
/// <param name="rng">An instance of a random number generator.</param>
/// <param name="data">A placeholder to generate random bytes into.</param>
/// <returns>A sequence whose elements are shuffled randomly.</returns>
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, RNGCryptoServiceProvider rng, byte[] data)
{
    var elements = source.ToArray();
    for (int i = elements.Length - 1; i >= 0; i--)
    {
        rng.GetBytes(data);
        var swapIndex = BitConverter.ToUInt32(data, 0) % (i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
    }
}

3

กำลังมองหาอัลกอริทึมอยู่หรือ? คุณสามารถใช้ShuffleListชั้นเรียนของฉัน:

class ShuffleList<T> : List<T>
{
    public void Shuffle()
    {
        Random random = new Random();
        for (int count = Count; count > 0; count--)
        {
            int i = random.Next(count);
            Add(this[i]);
            RemoveAt(i);
        }
    }
}

จากนั้นใช้อย่างนี้:

ShuffleList<int> list = new ShuffleList<int>();
// Add elements to your list.
list.Shuffle();

มันทำงานยังไง?

ลองทำรายการเรียงลำดับเริ่มต้นของจำนวนเต็ม 5 ตัวแรก: { 0, 1, 2, 3, 4 }ครั้งแรก:

countวิธีการเริ่มต้นโดยการนับจำนวนเท่ากับจำนวนองค์ประกอบและเรียกมันว่า จากนั้นcountเมื่อลดจำนวนลงในแต่ละขั้นตอนจะต้องใช้ตัวเลขสุ่มระหว่าง0และcountและย้ายไปยังจุดสิ้นสุดของรายการ

ในตัวอย่างทีละขั้นตอนต่อไปนี้รายการที่สามารถย้ายได้เป็นตัวเอียงรายการที่เลือกเป็นตัวหนา :

0 1 2 3 4
0 1 2 3 4
0 1 2 4 3
0 1 2 4 3
1 2 4 3 0
1 2 4 3 0
1 2 3 0 4
1 2 3 0 4
2 3 0 4 1
2 3 0 4 1
3 0 4 1 2


นั่นไม่ใช่ O (n) RemoveAt alone คือ O (n)
paparazzo

อืมดูเหมือนว่าคุณพูดถูกฉันไม่ดี! ฉันจะลบส่วนนั้น
SteeveDroz

1

อัลกอริทึมนี้สลับโดยสร้างค่าสุ่มใหม่สำหรับแต่ละค่าในรายการจากนั้นเรียงลำดับรายการด้วยค่าสุ่มเหล่านั้น คิดว่ามันเป็นการเพิ่มคอลัมน์ใหม่ลงในตารางในหน่วยความจำแล้วเติมด้วย GUID แล้วเรียงลำดับตามคอลัมน์นั้น ดูเหมือนวิธีที่มีประสิทธิภาพสำหรับฉัน (โดยเฉพาะกับแลมบ์ดาน้ำตาล!)


1

ไม่เกี่ยวข้องกันเล็กน้อย แต่นี่เป็นวิธีที่น่าสนใจ (แม้ว่ามันจะมากเกินไป แต่ก็มีการนำไปใช้จริง ๆ ) สำหรับการเล่นลูกเต๋าแบบสุ่ม!

Dice-O-Matic

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


1

ฉันจะบอกว่าคำตอบมากมายที่นี่เช่น "อัลกอริทึมนี้จะสับเปลี่ยนโดยการสร้างค่าสุ่มใหม่สำหรับแต่ละค่าในรายการจากนั้นการเรียงลำดับรายการด้วยค่าสุ่มเหล่านั้น" อาจผิดพลาดมาก!

ฉันคิดว่านี่ไม่ได้กำหนดค่าการสุ่มให้กับแต่ละองค์ประกอบของคอลเลกชันแหล่งที่มา แต่อาจมีอัลกอริทึมการเรียงลำดับที่เรียกใช้เช่น Quicksort ซึ่งจะเรียกฟังก์ชันการเปรียบเทียบโดยประมาณประมาณ n บันทึก n ครั้ง เรียงลำดับบางอย่างคาดหวังจริง ๆ นี้ฟังก์ชั่นการเปรียบเทียบจะมีเสถียรภาพและมักจะส่งกลับผลลัพธ์เดียวกัน!

เป็นไปไม่ได้ที่ IEnumerableSorter จะเรียกใช้ฟังก์ชั่นการเปรียบเทียบสำหรับแต่ละขั้นตอนของอัลกอริทึมของเช่น quicksort และในแต่ละครั้งที่เรียกใช้ฟังก์ชันx => r.Next()สำหรับพารามิเตอร์ทั้งสองโดยไม่ต้องแคชเหล่านี้!

ในกรณีนี้คุณอาจทำให้อัลกอริทึมการเรียงลำดับและทำให้แย่ลงกว่าความคาดหวังที่อัลกอริทึมถูกสร้างขึ้น แน่นอนว่าในที่สุดมันจะมีเสถียรภาพและคืนบางสิ่งบางอย่าง

ฉันอาจตรวจสอบในภายหลังโดยใส่การแก้ไขข้อบกพร่องในฟังก์ชัน "ถัดไป" ใหม่เพื่อดูว่าเกิดอะไรขึ้น ในตัวสะท้อนแสงฉันไม่สามารถรู้ได้ทันทีว่ามันทำงานอย่างไร


1
ไม่ใช่กรณีนี้: การแทนที่ภายในโมฆะ ComputeKeys (TElement [] องค์ประกอบนับ int); ประเภทการประกาศ: System.Linq.EnumerableSorter <TElement, TKey> แอสเซมบลี: System.Core, Version = 3.5.0.0 ฟังก์ชันนี้จะสร้างอาร์เรย์ก่อนด้วยคีย์ทั้งหมดที่ใช้หน่วยความจำก่อนที่จะเรียงลำดับพวกเขาอย่างรวดเร็ว
Christian

เป็นเรื่องดีที่รู้ - ยังคงเป็นเพียงรายละเอียดการใช้งานซึ่งอาจเปลี่ยนแปลงได้ในเวอร์ชันต่อไป!
Blorgbeard ออก

-5

เวลาเริ่มต้นเพื่อรันโค้ดโดยล้างเธรดและแคชทั้งหมดสำหรับการทดสอบใหม่

รหัสแรกไม่สำเร็จ มันทำงานบน LINQPad หากคุณติดตามเพื่อทดสอบรหัสนี้

Stopwatch st = new Stopwatch();
st.Start();
var r = new Random();
List<string[]> list = new List<string[]>();
list.Add(new String[] {"1","X"});
list.Add(new String[] {"2","A"});
list.Add(new String[] {"3","B"});
list.Add(new String[] {"4","C"});
list.Add(new String[] {"5","D"});
list.Add(new String[] {"6","E"});

//list.OrderBy (l => r.Next()).Dump();
list.OrderBy (l => Guid.NewGuid()).Dump();
st.Stop();
Console.WriteLine(st.Elapsed.TotalMilliseconds);

list.OrderBy (x => r.Next ()) ใช้ 38.6528 ms

list.OrderBy (x => Guid.NewGuid ()) ใช้ 36.7634 ms (แนะนำจาก MSDN)

หลังจากครั้งที่สองทั้งสองใช้ในเวลาเดียวกัน

แก้ไข: รหัสทดสอบบน Intel Core i7 4@2.1GHz, Ram 8 GB DDR3 @ 1600, HDD SATA 5200 รอบต่อนาทีพร้อม [ข้อมูล: www.dropbox.com/s/pbtmh5s9lw285kp/data]

using System;
using System.Runtime;
using System.Diagnostics;
using System.IO;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Threading;

namespace Algorithm
{
    class Program
    {
        public static void Main(string[] args)
        {
            try {
                int i = 0;
                int limit = 10;
                var result = GetTestRandomSort(limit);
                foreach (var element in result) {
                    Console.WriteLine();
                    Console.WriteLine("time {0}: {1} ms", ++i, element);
                }
            } catch (Exception e) {
                Console.WriteLine(e.Message);
            } finally {
                Console.Write("Press any key to continue . . . ");
                Console.ReadKey(true);
            }
        }

        public static IEnumerable<double> GetTestRandomSort(int limit)
        {
            for (int i = 0; i < 5; i++) {
                string path = null, temp = null;
                Stopwatch st = null;
                StreamReader sr = null;
                int? count = null;
                List<string> list = null;
                Random r = null;

                GC.Collect();
                GC.WaitForPendingFinalizers();
                Thread.Sleep(5000);

                st = Stopwatch.StartNew();
                #region Import Input Data
                path = Environment.CurrentDirectory + "\\data";
                list = new List<string>();
                sr = new StreamReader(path);
                count = 0;
                while (count < limit && (temp = sr.ReadLine()) != null) {
//                  Console.WriteLine(temp);
                    list.Add(temp);
                    count++;
                }
                sr.Close();
                #endregion

//              Console.WriteLine("--------------Random--------------");
//              #region Sort by Random with OrderBy(random.Next())
//              r = new Random();
//              list = list.OrderBy(l => r.Next()).ToList();
//              #endregion

//              #region Sort by Random with OrderBy(Guid)
//              list = list.OrderBy(l => Guid.NewGuid()).ToList();
//              #endregion

//              #region Sort by Random with Parallel and OrderBy(random.Next())
//              r = new Random();
//              list = list.AsParallel().OrderBy(l => r.Next()).ToList();
//              #endregion

//              #region Sort by Random with Parallel OrderBy(Guid)
//              list = list.AsParallel().OrderBy(l => Guid.NewGuid()).ToList();
//              #endregion

//              #region Sort by Random with User-Defined Shuffle Method
//              r = new Random();
//              list = list.Shuffle(r).ToList();
//              #endregion

//              #region Sort by Random with Parallel User-Defined Shuffle Method
//              r = new Random();
//              list = list.AsParallel().Shuffle(r).ToList();
//              #endregion

                // Result
//              
                st.Stop();
                yield return st.Elapsed.TotalMilliseconds;
                foreach (var element in list) {
                Console.WriteLine(element);
            }
            }

        }
    }
}

คำอธิบายผลลัพธ์: https://www.dropbox.com/s/9dw9wl259dfs04g/ResultDescription.PNG
สถิติผลลัพธ์: https://www.dropbox.com/s/ewq5ybtsvesme4d/ResultStat.PNG

สรุป:
สมมติว่า: LINQ OrderBy (r.Next ()) และ OrderBy (Guid.NewGuid ()) นั้นไม่ได้แย่ไปกว่าวิธีการสลับแบบกำหนดเองของผู้ใช้ในโซลูชันแรก

คำตอบ: พวกเขาขัดแย้งกัน


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

ขออภัยฉันต้องการทราบว่าอะไรคือพารามิเตอร์ของ Quicksort of Linq OrderBy ที่ดีกว่า? ฉันต้องการทดสอบประสิทธิภาพ อย่างไรก็ตามฉันคิดว่า int type มีความเร็วที่ดีกว่าสายของ Guid แต่ไม่ใช่ ฉันเข้าใจว่าทำไม MSDN แนะนำ โซลูชันแรกที่แก้ไขประสิทธิภาพเช่นเดียวกับ OrderBy กับอินสแตนซ์แบบสุ่ม
GMzo

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

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

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