การกำหนดความซับซ้อนสำหรับฟังก์ชั่นวนซ้ำ (สัญกรณ์ Big O)


267

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

int recursiveFun1(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun1(n-1);
}

int recursiveFun2(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun2(n-5);
}

int recursiveFun3(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun3(n/5);
}

void recursiveFun4(int n, int m, int o)
{
    if (n <= 0)
    {
        printf("%d, %d\n",m, o);
    }
    else
    {
        recursiveFun4(n-1, m+1, o);
        recursiveFun4(n-1, m, o+1);
    }
}

int recursiveFun5(int n)
{
    for (i = 0; i < n; i += 2) {
        // do something
    }

    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun5(n-5);
}

4
หากคุณไม่ต้องการผ่านการวิเคราะห์ทุกครั้งจะมีเทคนิคกล่องดำที่เรียกว่าวิธีการหลัก แต่ด้วยสมมติฐานที่ว่าการแยกอินพุตแบบเรียกซ้ำทั้งหมดนั้นมีขนาดเท่ากันในแต่ละอินสแตนซ์
Vivek Krishna


คำตอบ:


345

ความซับซ้อนของเวลาในรูปแบบ Big O สำหรับแต่ละฟังก์ชั่นอยู่ในลำดับที่เป็นตัวเลข:

  1. ฟังก์ชั่นแรกจะถูกเรียกว่าซ้ำ n ครั้งก่อนกรณีฐานถึงจึงO(n)มักจะเรียกว่าเชิงเส้น
  2. ฟังก์ชั่นที่สองเรียกว่า n-5 ในแต่ละครั้งดังนั้นเราจึงหักห้าจาก n ก่อนที่จะเรียกใช้ฟังก์ชัน แต่ n-5 ก็เช่นO(n)กัน (ที่จริงแล้วเรียกว่าคำสั่งของ n / 5 ครั้งและ O (n / 5) = O (n))
  3. ฟังก์ชันนี้คือ log (n) base 5 สำหรับทุกครั้งที่เราหารด้วย 5 ก่อนที่จะเรียกใช้ฟังก์ชันดังนั้นO(log(n))(base 5) มักเรียกว่าลอการิทึมและมักใช้สัญลักษณ์ Big O และการวิเคราะห์ความซับซ้อนใช้ฐาน 2
  4. ในครั้งที่สี่มันคือO(2^n)หรือเลขชี้กำลังเนื่องจากการเรียกใช้ฟังก์ชันแต่ละครั้งจะเรียกตัวเองสองครั้งเว้นแต่จะถูกเรียกซ้ำnครั้ง
  5. สำหรับฟังก์ชั่นสุดท้าย, for loop ใช้ n / 2 เนื่องจากเราเพิ่มขึ้น 2 และการเรียกซ้ำใช้ n-5 และเนื่องจาก for loop เรียกว่าซ้ำดังนั้นความซับซ้อนของเวลาจึงอยู่ใน (n-5) * (n / 2) = (2n-10) * n = 2n ^ 2- 10n เนื่องจากพฤติกรรมแบบอะซิมโทติคและการพิจารณาสถานการณ์กรณีที่เลวร้ายที่สุดหรือขอบเขตสูงสุดที่ O ใหญ่กำลังพยายามเรามีความสนใจในเทอมที่ใหญ่ที่สุดO(n^2)เท่านั้น

    ขอให้โชคดีในกลางภาคของคุณ;)


ด้านขวาของคุณประมาณห้า, n จะลดลงสำหรับ for loop แต่สำหรับอันที่สี่ฉันไม่คิดว่า n ^ 2 ของมันเหมือนต้นไม้ทุกครั้งที่คุณเรียกการสอบถามซ้ำสองครั้งดังนั้นมันควรจะเป็น 2 ^ n บวกนั่นคือของคุณ ตอบในความคิดเห็นก่อนหน้า
coder

2
@MJGwater ปล่อยให้เวลาทำงานของลูปคือ m เมื่อเรียกใช้ซ้ำ 1 ครั้งจะใช้เวลา m เพื่อดำเนินการลูป เมื่อเรียกซ้ำ 2 ครั้งวงก็จะวิ่งไป 2 ครั้งดังนั้นมันจึงใช้เวลา 2m ... และต่อไป ดังนั้นมันคือ '*' ไม่ใช่ '^'
bjc

3
@coder คำอธิบายสำหรับ 5 ดูเหมือนแปลก หากการเพิ่มขึ้น 2 ทำให้เกิดการn/2วนซ้ำของการforวนซ้ำเหตุใดการลดลง 5 ครั้งจึงไม่ส่งผลให้เกิดการn/5โทรซ้ำ สิ่งนี้จะยังคงส่งผลให้O(n^2)แต่ดูเหมือนคำอธิบายที่ใช้งานง่ายขึ้น เหตุใดจึงต้องผสมผสานการลบและการหารเมื่อพวกเขาจำเป็นต้องทำสิ่งเดียวกัน
แจ็ค

1
@coder ดังนั้นสำหรับ # 4 หากมีการเรียกซ้ำ 3 ครั้งในการกำหนดฟังก์ชั่นมันจะมีความซับซ้อนของเวลา O (3 ^ n)? และสำหรับการโทรซ้ำ 5 ครั้งจะเป็น O (5 ^ n) ถูกต้องไหม
rmutalik

1
@ แจ็คใช่ฉันยังสงสัยเหมือนกัน มันควรจะเป็นไม่ได้n/5 และท้ายที่สุดทั้งจะต้มลงไปn-5 O(N^2)
Anuj

128

สำหรับกรณีที่,n <= 0 ดังนั้นความซับซ้อนเวลาจะขึ้นอยู่กับเมื่อT(n) = O(1)n >= 0

เราจะพิจารณากรณีn >= 0ในส่วนด้านล่าง

1

T(n) = a + T(n - 1)

เมื่อ a คงที่

โดยการเหนี่ยวนำ:

T(n) = n * a + T(0) = n * a + b = O(n)

เมื่อ a, b คงที่

2

T(n) = a + T(n - 5)

เมื่อ a คงที่

โดยการเหนี่ยวนำ:

T(n) = ceil(n / 5) * a + T(k) = ceil(n / 5) * a + b = O(n)

โดยที่ a, b เป็นค่าคงที่และ k <= 0

3

T(n) = a + T(n / 5)

เมื่อ a คงที่

โดยการเหนี่ยวนำ:

T(n) = a * log5(n) + T(0) = a * log5(n) + b = O(log n)

เมื่อ a, b คงที่

4

T(n) = a + 2 * T(n - 1)

เมื่อ a คงที่

โดยการเหนี่ยวนำ:

T(n) = a + 2a + 4a + ... + 2^(n-1) * a + T(0) * 2^n 
     = a * 2^n - a + b * 2^n
     = (a + b) * 2^n - a
     = O(2 ^ n)

เมื่อ a, b คงที่

5

T(n) = n / 2 + T(n - 5)

โดยที่ n เป็นค่าคงที่

เขียนซ้ำn = 5q + rโดยที่ q และ r เป็นจำนวนเต็มและ r = 0, 1, 2, 3, 4

T(5q + r) = (5q + r) / 2 + T(5 * (q - 1) + r)

เรามีq = (n - r) / 5และตั้งแต่ r <5 เราสามารถพิจารณามันคงที่ดังนั้นq = O(n)

โดยการเหนี่ยวนำ:

T(n) = T(5q + r)
     = (5q + r) / 2 + (5 * (q - 1) + r) / 2 + ... + r / 2 +  T(r)
     = 5 / 2 * (q + (q - 1) + ... + 1) +  1 / 2 * (q + 1) * r + T(r)
     = 5 / 4 * (q + 1) * q + 1 / 2 * (q + 1) * r + T(r)
     = 5 / 4 * q^2 + 5 / 4 * q + 1 / 2 * q * r + 1 / 2 * r + T(r)

ตั้งแต่ r <4 เราสามารถหาค่าคงที่ b ดังนั้น b >= T(r)

T(n) = T(5q + r)
     = 5 / 2 * q^2 + (5 / 4 + 1 / 2 * r) * q + 1 / 2 * r + b
     = 5 / 2 * O(n ^ 2) + (5 / 4 + 1 / 2 * r) * O(n) + 1 / 2 * r + b
     = O(n ^ 2)

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

สำหรับหมายเลข 4 แม้ว่าผลลัพธ์จะเหมือนกัน แต่การเหนี่ยวนำไม่ควรเป็นดังต่อไปนี้ T (n) = a + 2T (n-1) = a + 2a + 4T (n-1) = 3a + 4a + 8T (n-1) = a * (2 ^ n - 1) + 2 ^ n * T (0) = a * (2 ^ n - 1) + b * 2 ^ n = (a + b) * 2 ^ n - a = O (2 ^ n)
Snowfish

27

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

Complexity = length of tree from root node to leaf node * number of leaf nodes
  1. ฟังก์ชั่นแรกจะมีความยาวnและจำนวนของโหนดปม1จึงมีความซับซ้อนn*1 = n
  2. ฟังก์ชั่นที่สองจะมีความยาวของn/5และจำนวนใบโหนดอีกครั้งเพื่อจะซับซ้อน1 n/5 * 1 = n/5มันควรจะประมาณn

  3. สำหรับฟังก์ชั่นที่สามเนื่องจากnจะถูกหารด้วย 5 ในการเรียกแบบเรียกซ้ำทุกครั้งความยาวของต้นไม้แบบเรียกซ้ำจะเท่ากับlog(n)(base 5)และจำนวนโหนดใบไม้อีกครั้ง 1 ดังนั้นความซับซ้อนจะมีlog(n)(base 5) * 1 = log(n)(base 5)

  4. สำหรับฟังก์ชั่นที่สี่เนื่องจากทุกโหนดจะมีโหนดย่อยสองโหนดจำนวนโหนดโหนดจะเท่ากับ(2^n)และความยาวของต้นไม้แบบเรียกซ้ำจะมีnความซับซ้อน(2^n) * nมาก แต่เนื่องจากnเป็นนัยสำคัญในด้านหน้าของ(2^n)มันสามารถละเลยและความซับซ้อนสามารถเพียง (2^n)แต่บอกว่าจะ

  5. สำหรับฟังก์ชั่นที่ห้ามีสององค์ประกอบที่แนะนำความซับซ้อน ความซับซ้อนที่นำมาใช้โดยธรรมชาติของฟังก์ชั่นซ้ำและความซับซ้อนนำมาใช้โดยforวงในแต่ละฟังก์ชั่น ทำคำนวณข้างต้นความซับซ้อนที่นำโดยธรรมชาติ recursive ของฟังก์ชั่นจะเป็นและความซับซ้อนเนื่องจากห่วง~ n ซับซ้อนรวมจะnn*n

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


คำตอบที่ยอดเยี่ยม! ฉันมีคำถามเกี่ยวกับฟังก์ชั่นที่สี่ หากมีการโทรซ้ำสามครั้งคำตอบนั้นจะเป็น (3 ^ n) หรือคุณยังจะพูด (2 ^ n)
Ben Forsrup

@Shubham: # 4 ดูเหมือนจะไม่ถูกต้องสำหรับฉัน ถ้าจำนวนของใบเป็น2^nแล้วความสูงของต้นไม้จะต้องไม่n log nความสูงจะเป็นเพียงlog nถ้าnแสดงจำนวนโหนดทั้งหมดในต้นไม้ แต่มันก็ไม่ได้
Julian A.

@BenForsrup: จะเป็น 3 ^ n เพราะทุกโหนดจะมีโหนดย่อยสามโหนด วิธีที่ดีที่สุดเพื่อให้แน่ใจเกี่ยวกับเรื่องนี้คือการวาดต้นไม้ซ้ำด้วยตัวเองด้วยค่าหุ่น
Shubham

# 2 ควรเป็น n-5 ไม่ใช่ n / 5
Fintasys

7

เราสามารถพิสูจน์ได้ทางคณิตศาสตร์ซึ่งเป็นสิ่งที่ฉันขาดหายไปในคำตอบข้างต้น

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

  1. T(n) = T(n-1) + 1หมายความว่าเวลาที่ใช้ในการดำเนินการให้เสร็จเท่ากับวิธีเดียวกัน แต่ด้วย n-1 ซึ่งเป็นT(n-1)และตอนนี้เราเพิ่ม+ 1เพราะถึงเวลาที่การดำเนินการทั่วไปจะแล้วเสร็จ (ยกเว้นT(n-1)) ตอนนี้เรากำลังจะไปหาT(n-1)ดังนี้: T(n-1) = T(n-1-1) + 1. ดูเหมือนว่าตอนนี้เราสามารถสร้างฟังก์ชั่นที่สามารถให้การซ้ำซ้อนบางอย่างเพื่อให้เราเข้าใจได้อย่างเต็มที่ เราจะวางด้านขวาของT(n-1) = ...แทนที่จะT(n-1)เป็นวิธีT(n) = ...ที่จะทำให้เรา: T(n) = T(n-1-1) + 1 + 1ซึ่งก็คือT(n) = T(n-2) + 2หรือในคำอื่น ๆ ที่เราต้องการที่จะหาที่ขาดหายไปของเราk: T(n) = T(n-k) + k. ขั้นตอนต่อไปคือการรับn-kและอ้างสิทธิ์n-k = 1เนื่องจากเมื่อสิ้นสุดการเรียกซ้ำจะใช้เวลา O (1) ทุกประการn<=0. k = n - 1จากสมการง่ายตอนนี้เรารู้ว่า สถานที่ Let 's kวิธีสุดท้ายของเรา: T(n) = T(n-k) + kที่จะให้เราT(n) = 1 + n - 1ซึ่งตรงหรือnO(n)
  2. เป็นเช่นเดียวกับ 1. O(n)คุณสามารถทดสอบได้ด้วยตัวคุณเองและดูว่าคุณได้รับ
  3. T(n) = T(n/5) + 1เมื่อก่อนเวลาสำหรับวิธีการนี้จะเสร็จสิ้นเท่ากับเวลาวิธีการเดียวกัน แต่มีซึ่งเป็นเหตุผลที่มันจะกระโดดไปn/5 T(n/5)ลองหาT(n/5)เหมือนใน 1: ซึ่งเป็นT(n/5) = T(n/5/5) + 1 T(n/5) = T(n/5^2) + 1มาวางไว้T(n/5)ข้างในT(n)สำหรับการคำนวณขั้นสุดท้าย: T(n) = T(n/5^k) + k. อีกครั้งเหมือนเมื่อก่อนn/5^k = 1ซึ่งn = 5^kตรงกับที่ถามว่าอะไรคือพลัง 5 จะทำให้เรา n คำตอบคือlog5n = k(บันทึกของฐาน 5) เรามาวางผลการวิจัยของเราในสิ่งT(n) = T(n/5^k) + kต่อไปT(n) = 1 + lognนี้ซึ่งก็คือO(logn)
  4. T(n) = 2T(n-1) + 1สิ่งที่เรามีตรงนี้ก็เหมือนกับเมื่อก่อน แต่คราวนี้เราเรียกวิธีนี้ซ้ำสองครั้งดังนั้นเราคูณด้วย 2 ลองหาT(n-1) = 2T(n-1-1) + 1อันไหนT(n-1) = 2T(n-2) + 1กัน สถานที่ต่อไปของเราเป็นก่อนที่จะขอวางการค้นพบของเรา: T(n) = 2(2T(n-2)) + 1 + 1ซึ่งเป็นที่ทำให้เราT(n) = 2^2T(n-2) + 2 T(n) = 2^kT(n-k) + kมาหาkโดยอ้างว่าn-k = 1มันคือk = n - 1อะไร ลองkทำดังนี้: T(n) = 2^(n-1) + n - 1ซึ่งเป็นคร่าวๆO(2^n)
  5. T(n) = T(n-5) + n + 1มันเกือบจะเหมือนกับ 4 แต่ตอนนี้เราเพิ่มnเพราะเรามีหนึ่งforวง ลองหาT(n-5) = T(n-5-5) + n + 1อันไหนT(n-5) = T(n - 2*5) + n + 1กัน สถานที่ Let 's มันT(n) = T(n-2*5) + n + n + 1 + 1)ซึ่งเป็นT(n) = T(n-2*5) + 2n + 2)และสำหรับ K: T(n) = T(n-k*5) + kn + k)อีกครั้ง: n-5k = 1ซึ่งเป็นที่เป็นประมาณn = 5k + 1 n = kนี้จะช่วยให้เราซึ่งเป็นประมาณT(n) = T(0) + n^2 + nO(n^2)

ตอนนี้ฉันขอแนะนำให้อ่านคำตอบที่เหลือซึ่งตอนนี้จะทำให้คุณมีมุมมองที่ดีขึ้น ขอให้โชคดีรับรางวัลใหญ่ของ O :)


1

กุญแจสำคัญที่นี่คือการเห็นภาพต้นไม้การโทร เมื่อเสร็จแล้วความซับซ้อนคือ:

nodes of the call tree * complexity of other code in the function

คำหลังสามารถคำนวณได้ในลักษณะเดียวกับที่เราทำเพื่อฟังก์ชั่นการวนซ้ำปกติ

แทนทั้งหมดโหนดของต้นไม้ที่สมบูรณ์จะถูกคำนวณเป็น

                  C^L - 1
                  -------  , when C>1
               /   C - 1
              /
 # of nodes =
              \    
               \ 
                  L        , when C=1

โดยที่ C คือจำนวนลูกของแต่ละโหนดและ L คือจำนวนระดับของทรี (รวมอยู่ในรูท)

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

ดังนั้นในตัวอย่างด้านบน:

  1. ทรีการเรียกใช้ที่นี่คือ C = 1, L = n + 1 ความซับซ้อนของฟังก์ชั่นที่เหลือคือ O (1) ดังนั้นความซับซ้อนทั้งหมดคือ L * O (1) = (n + 1) * O (1) = O (n)
n     level 1
n-1   level 2
n-2   level 3
n-3   level 4
... ~ n levels -> L = n
  1. ทรีเรียกที่นี่คือ C = 1, L = n / 5 ความซับซ้อนของฟังก์ชั่นที่เหลือคือ O (1) ดังนั้นความซับซ้อนทั้งหมดคือ L * O (1) = (n / 5) * O (1) = O (n)
n
n-5
n-10
n-15
... ~ n/5 levels -> L = n/5
  1. ทรีเรียกใช้ที่นี่คือ C = 1, L = log (n) ความซับซ้อนของฟังก์ชั่นที่เหลือคือ O (1) ดังนั้นความซับซ้อนทั้งหมดคือ L * O (1) = log5 (n) * O (1) = O (log (n))
n
n/5
n/5^2
n/5^3
... ~ log5(n) levels -> L = log5(n)
  1. ทรีเรียกใช้ที่นี่คือ C = 2, L = n ความซับซ้อนของฟังก์ชั่นที่เหลือคือ O (1) เวลานี้เราใช้สูตรเต็มจำนวนของโหนดในสายต้นไม้เพราะ C> 1 ดังนั้นความซับซ้อนทั้งหมดคือ (C ^ L-1) / (C-1) * O (1) = (2 ^ n - 1 ) * O (1) = O (2 ^ n)
               n                   level 1
      n-1             n-1          level 2
  n-2     n-2     n-2     n-2      ...
n-3 n-3 n-3 n-3 n-3 n-3 n-3 n-3    ...     
              ...                ~ n levels -> L = n
  1. ทรีเรียกที่นี่คือ C = 1, L = n / 5 ความซับซ้อนของฟังก์ชั่นที่เหลือคือ O (n) ดังนั้นความซับซ้อนทั้งหมดคือ L * O (1) = (n / 5) * O (n) = O (n ^ 2)
n
n-5
n-10
n-15
... ~ n/5 levels -> L = n/5
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.