GCD มีประสิทธิภาพมากที่สุดคืออะไร


26

ฉันรู้ว่าอัลกอริทึมของ Euclid เป็นอัลกอริทึมที่ดีที่สุดในการรับ GCD (ตัวหารร่วมมาก) ของรายการจำนวนเต็มบวก แต่ในทางปฏิบัติคุณสามารถเขียนโค้ดอัลกอริทึมนี้ได้หลายวิธี (ในกรณีของฉันฉันตัดสินใจใช้ Java แต่ C / C ++ อาจเป็นตัวเลือกอื่น)

ฉันต้องใช้รหัสที่มีประสิทธิภาพที่สุดเท่าที่จะเป็นไปได้ในโปรแกรมของฉัน

ในโหมดเรียกซ้ำคุณสามารถเขียน:

static long gcd (long a, long b){
    a = Math.abs(a); b = Math.abs(b);
    return (b==0) ? a : gcd(b, a%b);
  }

และในโหมดวนซ้ำดูเหมือนว่า:

static long gcd (long a, long b) {
  long r, i;
  while(b!=0){
    r = a % b;
    a = b;
    b = r;
  }
  return a;
}

นอกจากนี้ยังมีอัลกอริทึมแบบไบนารีสำหรับ GCD ซึ่งอาจมีการเข้ารหัสแบบนี้:

int gcd (int a, int b)
{
    while(b) b ^= a ^= b ^= a %= b;
    return a;
}

3
ฉันคิดว่านี่เป็นเรื่องส่วนตัวเกินไปและอาจเหมาะสมกว่าสำหรับ StackOverflow "มีประสิทธิภาพมากที่สุดในการปฏิบัติ" ขึ้นอยู่กับหลาย ๆ (แม้จะคาดเดาไม่ได้) ปัจจัยเช่นต้นแบบสถาปัตยกรรมลำดับชั้นของหน่วยความจำขนาดและรูปแบบของการป้อนข้อมูล ฯลฯ
Juho

5
นี่เป็นอัลกอริทึมเดียวกันที่แสดงในแบบวนซ้ำและวนซ้ำ ฉันคิดว่าความแตกต่างของพวกเขานั้นเล็กน้อยเนื่องจาก Euclid algorithm มาบรรจบกันอย่างรวดเร็ว เลือกหนึ่งที่เหมาะกับความต้องการของคุณ
แผ่น

6
คุณอาจต้องการลองทำโปรไฟล์ทั้งสองนี้ เนื่องจากเวอร์ชันแบบเรียกซ้ำเป็นแบบ tail call จึงไม่น่าเป็นไปได้ที่คอมไพเลอร์จะปล่อยรหัสเดียวกันเกือบทั้งหมด
หลุยส์

1
นี่มันผิด ควรอยู่ในขณะที่ b! = 0 แล้วกลับ a มิฉะนั้นจะเกิดข้อผิดพลาดในการหารด้วยศูนย์ และอย่าใช้การเรียกซ้ำถ้าคุณมี gcds ใหญ่จริงๆ ... คุณจะได้รับกองซ้อนและสถานะการทำงาน ... ทำไมไม่ไปทำซ้ำ?
Cris Stringfellow

4
โปรดทราบว่ามีอัลกอริทึม GCD ที่เร็วกว่าแบบไม่มีสัญญาณ เช่นen.wikipedia.org/wiki/Binary_GCD_algorithm
Neal Young

คำตอบ:


21

อัลกอริธึมทั้งสองของคุณนั้นเทียบเท่ากัน (อย่างน้อยสำหรับจำนวนเต็มบวกสิ่งที่เกิดขึ้นกับจำนวนเต็มลบในเวอร์ชันที่จำเป็นนั้นขึ้นอยู่กับซีแมนทิกส์ของ Java %ที่ฉันไม่รู้ด้วยใจ) ในรุ่น recursive ให้ฉันและฉันจะโต้แย้งของฉัน TH โทร recursive: ฉัน+ 1 = ฉันฉัน+ 1 = ฉันเมตรo ฉันaผมผมผม

aผม+1=ผมผม+1=aผมม.โอdผม

ในรุ่นความจำเป็นให้' ฉันและ' ฉันจะเป็นค่าของตัวแปรที่และที่จุดเริ่มต้นของผมทวนของวงวันที่ a ฉัน+ 1 = b ฉัน b ฉัน+ 1 = a ฉัน m o d b ฉันaผม'ผม'abผม

aผม+1'=ผม'ผม+1'=aผม'ม.โอdผม'

สังเกตความคล้ายคลึงกัน? เวอร์ชันที่จำเป็นของคุณและเวอร์ชันที่เรียกซ้ำของคุณกำลังคำนวณค่าเดียวกันทั้งหมด นอกจากนี้พวกเขาตอนท้ายทั้งสองในเวลาเดียวกันเมื่อฉัน = 0 (resp. ' ฉัน = 0 ) ดังนั้นพวกเขาดำเนินการจำนวนเดียวกันของการทำซ้ำ ดังนั้นการพูดแบบอัลกอริทึมจึงไม่มีความแตกต่างระหว่างทั้งสอง ความแตกต่างใด ๆ จะเป็นเรื่องของการใช้งานขึ้นอยู่กับคอมไพเลอร์ฮาร์ดแวร์ที่ใช้งานและอาจเป็นไปได้ว่าระบบปฏิบัติการและโปรแกรมอื่น ๆ กำลังทำงานพร้อมกันaผม=0aผม'=0

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


8

สำหรับตัวเลขที่มีขนาดเล็กอัลกอริทึม GCD ไบนารี่ก็เพียงพอ

GMP ซึ่งเป็นห้องสมุดที่ได้รับการบำรุงรักษาอย่างดีและมีการทดสอบในโลกแห่งความเป็นจริงจะเปลี่ยนเป็นอัลกอริธึมครึ่ง GCD พิเศษหลังจากผ่านเกณฑ์พิเศษ Lehmer ใช้การคูณเมทริกซ์เพื่อปรับปรุงตามอัลกอริทึม Euclidian มาตรฐาน ตามเอกสารที่มีเวลาในการทำงานของทั้งสอง asymptotic HGCD และ GCD เป็นO(M(N)*log(N))ที่M(N)เป็นเวลาสำหรับการคูณตัวเลขสอง N-กิ่ง

รายละเอียดเกี่ยวกับขั้นตอนวิธีการของพวกเขาสามารถพบได้ที่นี่


ลิงค์ไม่ได้ให้รายละเอียดครบถ้วนและไม่ได้ระบุว่า "ขา" คืออะไร ...
einpoklum - คืนสถานะโมนิก้า


2

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

ฉันควรทราบด้วยว่าอัลกอริทึม GCD ที่เร็วที่สุดไม่ใช่อัลกอริทึมของ Euclid อัลกอริทึมของ Lehmerนั้นเร็วขึ้นเล็กน้อย


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

1

ก่อนอื่นอย่าใช้การวนซ้ำเพื่อแทนที่การวนซ้ำที่แน่น มันช้า อย่าพึ่งพาคอมไพเลอร์เพื่อปรับให้เหมาะสม ประการที่สองในรหัสของคุณคุณเรียก Math.abs () ภายในการโทรซ้ำทุกครั้งซึ่งไม่มีประโยชน์

ในวงของคุณคุณสามารถหลีกเลี่ยงตัวแปรชั่วคราวและสลับ a และ b ตลอดเวลา

int gcd(int a, int b){
    if( a<0 ) a = -a;
    if( b<0 ) b = -b;
    while( b!=0 ){
        a %= b;
        if( a==0 ) return b;
        b %= a;
    }
    return a;
}

การแลกเปลี่ยนโดยใช้ a ^ = b ^ = a ^ = b ทำให้แหล่งข้อมูลสั้นลง แต่ใช้คำสั่งมากมายในการดำเนินการ มันจะช้ากว่าการแลกเปลี่ยนที่น่าเบื่อด้วยตัวแปรชั่วคราว


3
“ หลีกเลี่ยงการเกิดซ้ำ มันช้า” - นำเสนอตามคำแนะนำทั่วไปนี่เป็นของปลอม มันขึ้นอยู่กับคอมไพเลอร์ โดยทั่วไปแม้จะมีคอมไพเลอร์ที่ไม่ปรับการเรียกซ้ำให้เหมาะสม แต่ก็ไม่ได้ช้าเพียงใช้งานสแต็ก
Gilles 'ดังนั้นหยุดความชั่วร้าย'

3
แต่สำหรับรหัสสั้น ๆ เช่นนี้ความแตกต่างก็สำคัญ กองการบริโภคหมายถึงการเขียนและอ่านจากหน่วยความจำ นั่นคือช้า รหัสข้างต้นทำงานใน 2 ลงทะเบียน การเรียกซ้ำหมายถึงการโทรซึ่งยาวกว่าการกระโดดตามเงื่อนไข การเรียกแบบเรียกซ้ำเป็นสิ่งที่ยากกว่าสำหรับการคาดคะเนสาขา
Florian F

-2

สำหรับตัวเลขขนาดเล็ก % เป็นการดำเนินการที่ค่อนข้างแพงบางทีอาจจะเรียกซ้ำได้ง่ายกว่า

GCD[a,b] := Which[ 
   a==b , Return[a],
   b > a, Return[ GCD[a, b-a]],
   a > b, Return[ GCD[b, a-b]]
];

เร็วกว่าไหม (ขออภัยรหัส Mathematica ไม่ใช่ C ++)


มันดูไม่ถูกต้อง สำหรับ b == 1 ควรกลับ 1 และ GCD [2,1000000000] จะช้า
Florian F

อ่าใช่ฉันทำผิดไป คงที่ (ฉันคิดว่า) และชี้แจง
ต่อ Alexandersson

โดยปกติ GCD [a, 0] ควรกลับด้วย ขอแสดงความนับถือลูปตลอดกาล
Florian F

ฉันกำลังลงคะแนนเนื่องจากคำตอบของคุณมีรหัสเท่านั้น เราชอบที่จะมุ่งเน้นความคิดในเว็บไซต์นี้ ตัวอย่างเช่นเหตุใด% การดำเนินการจึงมีราคาแพง การคาดเดารหัสชิ้นนี้ไม่ใช่คำตอบที่ดีสำหรับเว็บไซต์นี้ในความคิดของฉัน
Juho

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

-2

อัลกอริทึมแบบยุคลิดมีประสิทธิภาพมากที่สุดสำหรับการคำนวณ GCD:

gcd แบบคงที่ยาว (ยาว a, ยาว b)
{
ถ้า (ข == 0)
กลับมา;
อื่น
ส่งคืน gcd (, a% b);
}

ตัวอย่าง:-

ให้ A = 16, B = 10
GCD (16, 10) = GCD (10, 16% 10) = GCD (10, 6)
GCD (10, 6) = GCD (6, 10% 6) = GCD (6, 4)
GCD (6, 4) = GCD (4, 6% 4) = GCD (4, 2)
GCD (4, 2) = GCD (2, 4% 2) = GCD (2, 0)


เนื่องจาก B = 0 ดังนั้น GCD (2, 0) จะส่งคืน 2 

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