บิ๊กโอคุณคำนวณ / ประเมินมันอย่างไร


881

คนส่วนใหญ่ที่มีปริญญาใน CS จะรู้แน่นอนว่าBig O หมายถึงอะไร มันช่วยให้เราวัดว่าอัลกอริทึมปรับขนาดได้ดีแค่ไหน

แต่ฉันอยากรู้คุณจะคำนวณหรือประมาณความซับซ้อนของอัลกอริทึมของคุณได้อย่างไร


4
บางทีคุณอาจไม่จำเป็นต้องปรับปรุงความซับซ้อนของอัลกอริทึมของคุณ แต่อย่างน้อยคุณควรจะสามารถคำนวณมันได้เพื่อตัดสินใจ ...
Xavier Nodet

5
ฉันพบสิ่งนี้เป็นคำอธิบายที่ชัดเจนมากของ Big O, Big Omega และ Big Theta: xoax.net/comp/sci/algorithms/Lesson6.php
Sam Dutton

33
-1: ถอนหายใจการทารุณ BigOh อีกครั้ง BigOh เป็นเพียงขอบเขตบนเชิงซีมโทติคและสามารถนำไปใช้เพื่ออะไรก็ได้และไม่ใช่แค่ CS ที่เกี่ยวข้อง พูดคุยเกี่ยวกับ BigOh ราวกับว่ามีสิ่งหนึ่งที่ไม่เหมือนใครคือไม่มีความหมาย (อัลกอริธึมเชิงเส้นเวลายังเป็น O (n ^ 2), O (n ^ 3) ฯลฯ ) การบอกว่าช่วยให้เราสามารถวัด ประสิทธิภาพได้ทำให้เข้าใจผิดเช่นกัน นอกจากนี้สิ่งที่มีการเชื่อมโยงไปยังคลาสที่ซับซ้อน? หากสิ่งที่คุณสนใจคือเทคนิคในการคำนวณเวลาทำงานของอัลกอริทึมนั้นมีความเกี่ยวข้องกันอย่างไร

34
Big-O ไม่ได้วัดประสิทธิภาพ มันวัดว่าอัลกอริธึมปรับขนาดได้ดีเพียงใด (มันสามารถนำไปใช้กับสิ่งอื่นนอกเหนือจากขนาดได้เช่นกัน แต่นั่นคือสิ่งที่เราน่าสนใจที่นี่) และนั่นเป็นเพียงแบบไม่แสดงอาการดังนั้นถ้าคุณโชคดีกว่า O อาจช้าลง (หาก Big-O นำไปใช้กับรอบ) ที่แตกต่างกันจนกว่าคุณจะถึงจำนวนมาก
ILoveFortran

4
การเลือกอัลกอริทึมบนพื้นฐานของความซับซ้อน Big-O มักเป็นส่วนสำคัญของการออกแบบโปรแกรม แน่นอนที่สุดไม่ใช่กรณีของ 'การปรับให้เหมาะสมก่อนวัยอันควร' ซึ่งในกรณีใด ๆ เป็นคำพูดที่เลือกสรรที่ละเมิดมาก
user207421

คำตอบ:


1480

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


ไม่มีกระบวนการทางกลที่สามารถใช้เพื่อรับ BigOh

ในฐานะ "ตำรา" ในการรับBigOhจากโค้ดบางส่วนคุณต้องรู้ก่อนว่าคุณกำลังสร้างสูตรคณิตศาสตร์เพื่อนับจำนวนขั้นตอนการคำนวณที่ได้รับจากการป้อนข้อมูลในบางขนาด

จุดประสงค์คือง่าย ๆ : เพื่อเปรียบเทียบอัลกอริธึมจากมุมมองเชิงทฤษฎีโดยไม่จำเป็นต้องรันโค้ด ยิ่งจำนวนขั้นตอนน้อยลงเท่าใด

ตัวอย่างเช่นสมมติว่าคุณมีรหัสชิ้นนี้:

int sum(int* data, int N) {
    int result = 0;               // 1

    for (int i = 0; i < N; i++) { // 2
        result += data[i];        // 3
    }

    return result;                // 4
}

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

Number_Of_Steps = f(N)

ดังนั้นเราจึงมีf(N)ฟังก์ชั่นเพื่อนับจำนวนขั้นตอนการคำนวณ อินพุตของฟังก์ชันคือขนาดของโครงสร้างที่จะประมวลผล มันหมายความว่าฟังก์ชั่นนี้เรียกว่าเช่น:

Number_Of_Steps = f(data.length)

พารามิเตอร์Nรับdata.lengthค่า f()ตอนนี้เราต้องนิยามที่แท้จริงของฟังก์ชั่น สิ่งนี้ทำจากซอร์สโค้ดซึ่งแต่ละบรรทัดที่น่าสนใจจะมีหมายเลขตั้งแต่ 1 ถึง 4

มีหลายวิธีในการคำนวณ BigOh จากจุดนี้ไปข้างหน้าเราจะสมมติว่าทุกประโยคที่ไม่ได้ขึ้นอยู่กับขนาดของข้อมูลที่ป้อนจะมีCขั้นตอนการคำนวณจำนวนคงที่

เราจะเพิ่มจำนวนแต่ละขั้นตอนของฟังก์ชั่นและการประกาศตัวแปรท้องถิ่นหรือคำสั่งกลับขึ้นอยู่กับขนาดของdataอาร์เรย์

นั่นหมายความว่าบรรทัดที่ 1 และ 4 ใช้จำนวน C ของแต่ละขั้นตอนและฟังก์ชั่นจะเป็นดังนี้:

f(N) = C + ??? + C

ส่วนถัดไปคือการกำหนดค่าของforคำสั่ง จำไว้ว่าเรากำลังนับจำนวนขั้นตอนการคำนวณซึ่งหมายความว่าเนื้อหาของforคำสั่งนั้นได้รับการดำเนินการNครั้ง นั่นเป็นเหมือนการเพิ่มC, Nครั้ง:

f(N) = C + (C + C + ... + C) + C = C + N * C + C

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

เพื่อให้ได้ BigOh ที่แท้จริงเราจำเป็นต้องมีการวิเคราะห์ Asymptoticของฟังก์ชั่น สิ่งนี้ทำคร่าวๆดังนี้:

  1. Take away Cค่าคงที่ทั้งหมด
  2. จากการf()ได้รับpolynomiumstandard formในของมัน
  3. แบ่งเงื่อนไขของพหุนามและจัดเรียงตามอัตราการเติบโต
  4. เก็บหนึ่งที่เติบโตใหญ่เมื่อวิธีการNinfinity

เราf()มีสองเทอม:

f(N) = 2 * C * N ^ 0 + 1 * C * N ^ 1

กำจัดCค่าคงที่และส่วนที่ซ้ำซ้อนทั้งหมดออกไป:

f(N) = 1 + N ^ 1

เนื่องจากระยะที่ผ่านมาเป็นหนึ่งในที่เติบโตใหญ่เมื่อf()เข้าใกล้อินฟินิตี้ (คิดว่าในข้อ จำกัด ) นี้เป็นอาร์กิวเมนต์ BigOh และsum()ฟังก์ชั่นมี BigOh ของ:

O(N)

มีเทคนิคเล็กน้อยในการแก้ปัญหาที่ยุ่งยาก: ใช้การสรุปเมื่อใดก็ตามที่คุณทำได้

เป็นตัวอย่างรหัสนี้สามารถแก้ไขได้อย่างง่ายดายโดยใช้การสรุป:

for (i = 0; i < 2*n; i += 2) {  // 1
    for (j=n; j > i; j--) {     // 2
        foo();                  // 3
    }
}

foo()สิ่งแรกที่คุณจะต้องถามว่าเป็นคำสั่งของการดำเนินการของ ในขณะที่ปกติจะเป็นO(1)คุณต้องถามอาจารย์ของคุณเกี่ยวกับเรื่องนี้ O(1)หมายความว่า (เกือบส่วนใหญ่) คงเป็นอิสระจากขนาดCN

forคำชี้แจงเกี่ยวกับจำนวนหนึ่งประโยคเป็นเรื่องยุ่งยาก ในขณะที่ดัชนีสิ้นสุดที่2 * Nการเพิ่มขึ้นจะกระทำโดยสอง นั่นหมายความว่าครั้งแรกที่forได้รับการดำเนินการเพียงNขั้นตอนและเราต้องหารจำนวนสอง

f(N) = Summation(i from 1 to 2 * N / 2)( ... ) = 
     = Summation(i from 1 to N)( ... )

จำนวนประโยคสองคือแม้ trickier iเพราะมันขึ้นอยู่กับมูลค่าของ ลองดู: ดัชนีที่ฉันใช้ค่า: 0, 2, 4, 6, 8, ... , 2 * N, และที่สองforได้รับการดำเนินการ: N ครั้งแรกที่หนึ่ง, N - 2 ที่สอง, N - 4 ขั้นตอนที่สาม ... จนถึงระยะ N / 2 ซึ่งครั้งที่สองforไม่เคยถูกประหารชีวิต

ในสูตรนั่นหมายถึง:

f(N) = Summation(i from 1 to N)( Summation(j = ???)(  ) )

อีกครั้งเรามีการนับจำนวนของขั้นตอนที่ และตามคำนิยามการสรุปทุกครั้งควรเริ่มต้นที่หนึ่งเสมอและจบด้วยตัวเลขที่ใหญ่กว่าหรือเท่ากับหนึ่ง

f(N) = Summation(i from 1 to N)( Summation(j = 1 to (N - (i - 1) * 2)( C ) )

(เราสมมติว่าfoo()เป็นO(1)และดำเนินการCตามขั้นตอน)

เรามีปัญหาที่นี่: เมื่อiนำค่าN / 2 + 1ขึ้นด้านบนการรวมภายในสิ้นสุดลงด้วยจำนวนลบ! นั่นเป็นไปไม่ได้และผิด เราจำเป็นต้องแยกบวกในสองเป็นจุดที่สำคัญช่วงเวลาที่ต้องใช้เวลาiN / 2 + 1

f(N) = Summation(i from 1 to N / 2)( Summation(j = 1 to (N - (i - 1) * 2)) * ( C ) ) + Summation(i from 1 to N / 2) * ( C )

ตั้งแต่ช่วงการพิจาณาi > N / 2ด้านในforจะไม่ได้รับการประหารชีวิตและเรากำลังสมมติความซับซ้อนในการประมวลผล C คงที่ในร่างกายของมัน

ตอนนี้การสรุปสามารถทำให้ง่ายขึ้นโดยใช้กฎเอกลักษณ์:

  1. การรวม (w จาก 1 ถึง N) (C) = N * C
  2. การรวม (w จาก 1 ถึง N) (A (+/-) B) = การรวม (w จาก 1 ถึง N) (A) (+/-) การรวม (w จาก 1 ถึง N) (B)
  3. การรวม (w จาก 1 ถึง N) (w * C) = C * การรวม (w จาก 1 ถึง N) (w) (w) (C เป็นค่าคงที่อิสระจากw)
  4. การรวม (w จาก 1 ถึง N) (w) = (N * (N + 1)) / 2

ใช้พีชคณิตบางส่วน:

f(N) = Summation(i from 1 to N / 2)( (N - (i - 1) * 2) * ( C ) ) + (N / 2)( C )

f(N) = C * Summation(i from 1 to N / 2)( (N - (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (Summation(i from 1 to N / 2)( N ) - Summation(i from 1 to N / 2)( (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2)( i - 1 )) + (N / 2)( C )

=> Summation(i from 1 to N / 2)( i - 1 ) = Summation(i from 1 to N / 2 - 1)( i )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2 - 1)( i )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N / 2 - 1) * (N / 2 - 1 + 1) / 2) ) + (N / 2)( C )

=> (N / 2 - 1) * (N / 2 - 1 + 1) / 2 = 

   (N / 2 - 1) * (N / 2) / 2 = 

   ((N ^ 2 / 4) - (N / 2)) / 2 = 

   (N ^ 2 / 8) - (N / 4)

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N ^ 2 / 8) - (N / 4) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - ( (N ^ 2 / 4) - (N / 2) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - (N ^ 2 / 4) + (N / 2)) + (N / 2)( C )

f(N) = C * ( N ^ 2 / 4 ) + C * (N / 2) + C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + 2 * C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + C * N

f(N) = C * 1/4 * N ^ 2 + C * N

และ BigOh คือ:

O(N²)

6
@ อาร์เธอร์นั่นจะเป็น O (N ^ 2) เพราะคุณจะต้องใช้หนึ่งวงในการอ่านคอลัมน์ทั้งหมดและอีกวงหนึ่งเพื่ออ่านแถวทั้งหมดของคอลัมน์ใดคอลัมน์หนึ่ง
Abhishek Dey Das

@ อาร์เธอร์: มันขึ้นอยู่กับ มันเป็นเรื่องO(n)ที่nเป็นจำนวนขององค์ประกอบหรือO(x*y)ที่xและyมีขนาดของอาร์เรย์ Big-oh คือ "สัมพันธ์กับอินพุต" ดังนั้นจึงขึ้นอยู่กับอินพุตของคุณ
Mooing Duck

1
คำตอบที่ดี แต่ฉันติดจริงๆ Summation (i จาก 1 ถึง N / 2) (N) เปลี่ยนเป็นอย่างไร (N ^ 2/2)
Parsa

2
@ParsaAkbari ตามกฎทั่วไปผลรวม (i จาก 1 ถึง a) (b) คือ a * b นี่เป็นอีกวิธีหนึ่งในการบอกว่า b + b + ... (a ครั้ง) + b = a * b (โดยนิยามสำหรับคำจำกัดความของการคูณจำนวนเต็ม)
Mario Carneiro

ไม่เกี่ยวข้อง แต่เพียงเพื่อหลีกเลี่ยงความสับสนมีข้อผิดพลาดเล็กน้อยในประโยคนี้: "ดัชนี i ใช้ค่า: 0, 2, 4, 6, 8, ... , 2 * N" ดัชนีฉันไปถึง 2 * N - 2 จริง ๆ แล้วลูปจะหยุดทำงาน
อัลเบิร์ต

201

Big O ให้ขอบเขตความซับซ้อนของเวลาสำหรับอัลกอริทึม โดยปกติจะใช้ร่วมกับชุดข้อมูลการประมวลผล (รายการ) แต่สามารถใช้ได้ที่อื่น

ตัวอย่างบางส่วนของวิธีการใช้งานในรหัส C

สมมติว่าเรามีองค์ประกอบ n จำนวนมากมาย

int array[n];

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

x = array[0];

หากเราต้องการค้นหาหมายเลขในรายการ:

for(int i = 0; i < n; i++){
    if(array[i] == numToFind){ return i; }
}

นี่จะเป็น O (n) เนื่องจากอย่างมากเราจะต้องตรวจสอบรายการทั้งหมดเพื่อค้นหาหมายเลขของเรา Big-O ยังคงเป็น O (n) แม้ว่าเราอาจพบว่าหมายเลขของเราเป็นครั้งแรกที่พยายามแล้ววิ่งผ่านลูปหนึ่งครั้งเพราะ Big-O อธิบายขอบเขตบนของอัลกอริทึม (โอเมก้าสำหรับขอบเขตที่ต่ำกว่า .

เมื่อเราไปถึงลูปซ้อนกัน:

for(int i = 0; i < n; i++){
    for(int j = i; j < n; j++){
        array[j] += 2;
    }
}

นี่คือ O (n ^ 2) เนื่องจากแต่ละรอบของวงนอก (O (n)) เราต้องผ่านรายการทั้งหมดอีกครั้งดังนั้น n จึงคูณเราด้วย n กำลังสอง

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


คำอธิบายที่ดี! ดังนั้นถ้ามีคนบอกว่าอัลกอริทึมของเขามีความซับซ้อน O (n ^ 2) หมายความว่าเขาจะใช้ลูปซ้อนกันหรือไม่
Navaneeth KN

2
ไม่ใช่จริงๆแง่มุมใด ๆ ที่นำไปสู่เวลา n กำลังสองจะถูกพิจารณาเป็น n ^ 2
asyncwait

@NavaneethKN: คุณจะไม่เห็นลูปซ้อนอยู่เสมอเนื่องจากการเรียกใช้ฟังก์ชันสามารถทำได้> O(1)ทำงานด้วยตนเอง ในมาตรฐาน API C เช่นbsearchเป็นอย่างโดยเนื้อแท้O(log n), strlenเป็นO(n)และqsortเป็นO(n log n)(ในทางเทคนิคมันไม่มีการค้ำประกันและ quicksort ตัวเองมีความซับซ้อนกรณีที่เลวร้ายที่สุดของO(n²)แต่สมมติว่าคุณlibcเขียนจะไม่ปัญญาอ่อนซับซ้อนกรณีเฉลี่ยอยู่O(n log n)และจะใช้ กลยุทธ์การเลือกเดือยที่ช่วยลดโอกาสในการตีO(n²)กรณี) และทั้งคู่bsearchและqsortอาจแย่ลงถ้าฟังก์ชั่นเปรียบเทียบเป็นพยาธิวิทยา
ShadowRanger

95

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

นี่คือบางกรณีที่พบบ่อยที่สุดยกมาจากhttp://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions :

O (1) - การพิจารณาว่าตัวเลขเป็นเลขคู่หรือคี่ ใช้ตารางการค้นหาขนาดคงที่หรือตารางแฮช

O (logn) - การค้นหารายการในอาร์เรย์ที่เรียงลำดับด้วยการค้นหาแบบไบนารี

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

O (n 2 ) - การคูณตัวเลขสองหลักด้วยอัลกอริทึมแบบง่าย การเพิ่มเมทริกซ์สอง n n n; เรียงลำดับฟองหรือเรียงแทรก

O (n 3 ) - การคูณเมทริกซ์สอง n × n ด้วยอัลกอริทึมแบบง่าย

O (c n ) - ค้นหาโซลูชัน (แน่นอน) สำหรับปัญหาพนักงานขายเดินทางโดยใช้การเขียนโปรแกรมแบบไดนามิก; การพิจารณาว่าสองงบเชิงตรรกะจะเทียบเท่าโดยใช้กำลังดุร้าย

O (n!) - การแก้ไขปัญหาพนักงานขายที่เดินทางผ่านการค้นหาแบบไร้กำลัง

O (n n ) - มักใช้แทน O (n!) เพื่อให้ได้สูตรที่ง่ายขึ้นสำหรับความซับซ้อนเชิงซีมโทติค


ทำไมไม่ใช้x&1==1เพื่อตรวจสอบความแปลก?
Samy Bencherif

2
@SamyBencherif: นั่นจะเป็นวิธีการทั่วไปในการตรวจสอบ (จริง ๆ แล้วแค่การทดสอบx & 1จะเพียงพอไม่จำเป็นต้องตรวจสอบ== 1ใน C x&1==1ได้รับการประเมินว่าเป็นx&(1==1) เพราะความสำคัญของผู้ดำเนินการดังนั้นจึงเหมือนกับการทดสอบจริง ๆx&1) ฉันคิดว่าคุณเข้าใจผิดคำตอบว่า; มีเครื่องหมายจุดคู่อยู่ที่นั่นไม่ใช่เครื่องหมายจุลภาค มันไม่ได้บอกว่าคุณต้องใช้ตารางการค้นหาสำหรับการทดสอบแบบสม่ำเสมอ/ คี่การพูดทั้งการทดสอบแบบสม่ำเสมอและแบบทดสอบ O(1)
ShadowRanger

ฉันไม่รู้เกี่ยวกับการอ้างสิทธิ์ในการใช้งานในประโยคสุดท้าย แต่ใครก็ตามที่เปลี่ยนชั้นเรียนโดยผู้อื่นที่ไม่เทียบเท่า คลาส O (n!) มี แต่ใหญ่กว่า O (n ^ n) อย่างเคร่งครัด การเทียบเท่าที่แท้จริงคือ O (n!) = O (n ^ ne ^ {- n} sqrt (n))
conditionalMethod

43

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

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

โปรดทราบว่าค่าคงที่ที่ซ่อนอยู่นั้นขึ้นอยู่กับการนำไปใช้

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

มีความซับซ้อนของเวลาที่แตกต่างกัน:

  • กรณีที่เลวร้ายที่สุด (มักจะง่ายที่สุดที่จะคิดออก แต่ไม่ได้มีความหมายเสมอ)
  • กรณีเฉลี่ย (โดยทั่วไปจะยากกว่าที่จะคิดออก ... )

  • ...

การแนะนำที่ดีคือการแนะนำการวิเคราะห์อัลกอริทึมโดย R. Sedgewick และ P. Flajolet

ดังที่คุณพูดpremature optimisation is the root of all evilและควรใช้การทำโปรไฟล์ (ถ้าเป็นไปได้) เสมอเมื่อปรับรหัสให้เหมาะสม มันยังสามารถช่วยคุณกำหนดความซับซ้อนของอัลกอริทึมของคุณ


3
ในคณิตศาสตร์ O (.) หมายถึงขอบเขตบนและ theta (.) หมายถึงคุณมีขอบด้านบนและด้านล่าง คำจำกัดความแตกต่างกันจริง ๆ ใน CS หรือว่าเป็นเพียงการใช้สัญลักษณ์ร่วมกันในทางที่ผิด? ตามคำจำกัดความทางคณิตศาสตร์ sqrt (n) มีทั้ง O (n) และ O (n ^ 2) ดังนั้นจึงไม่ใช่กรณีที่มีบาง n หลังจากที่ฟังก์ชัน O (n) มีขนาดเล็กลง
Douglas Zare

28

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

นอกจากนี้ฉันต้องการเพิ่มว่ามันจะทำอย่างไรสำหรับฟังก์ชั่นซ้ำ :

สมมติว่าเรามีฟังก์ชั่นเช่น ( รหัสโครงร่าง ):

(define (fac n)
    (if (= n 0)
        1
            (* n (fac (- n 1)))))

ซึ่งคำนวณแฟกทอเรียลแบบวนซ้ำตามจำนวนที่กำหนด

ขั้นตอนแรกคือการลองและกำหนดลักษณะการทำงานสำหรับร่างกายของฟังก์ชันเฉพาะในกรณีนี้ไม่มีสิ่งใดที่พิเศษทำในร่างกายเพียงแค่การคูณ (หรือการกลับมาของค่า 1)

ดังนั้นประสิทธิภาพสำหรับร่างกายคือ: O (1) (ค่าคงที่)

ลองถัดไปและกำหนดนี้สำหรับจำนวนของสาย recursive ในกรณีนี้เรามีการโทรซ้ำแบบ n-1

ดังนั้นประสิทธิภาพสำหรับการโทรแบบเรียกซ้ำคือ: O (n-1) (คำสั่งซื้อคือ n เมื่อเราทิ้งส่วนที่ไม่สำคัญออกไป)

จากนั้นนำสองสิ่งนี้มารวมกันและคุณจะได้รับฟังก์ชั่นการเรียกซ้ำทั้งหมด:

1 * (n-1) = O (n)


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


Sven, ฉันไม่แน่ใจว่าวิธีการตัดสินความซับซ้อนของฟังก์ชั่นแบบเรียกซ้ำนั้นจะทำงานให้กับสิ่งที่ซับซ้อนมากขึ้นเช่นการค้นหา / สรุปผล / บน / ล่าง / บางอย่างบนต้นไม้ไบนารี แน่นอนว่าคุณสามารถให้เหตุผลเกี่ยวกับตัวอย่างง่ายๆและหาคำตอบ แต่ฉันคิดว่าคุณจะต้องทำคณิตศาสตร์สำหรับคนที่เรียกซ้ำ?
Peteter

3
+1 สำหรับการสอบถามซ้ำอีกครั้ง ... อันนี้ก็ยังสวยงาม: "... แม้กระทั่งอาจารย์ก็สนับสนุนให้เราคิด ... " :)
TT_

ใช่มันดีมาก ฉันมักจะคิดว่าเป็นเช่นนี้ยิ่งคำใน O (.. ) สูงขึ้นงานที่คุณทำ / เครื่องจักรกำลังทำอยู่ การคิดในขณะที่เกี่ยวข้องกับบางสิ่งอาจเป็นการประมาณ แต่ก็เป็นขอบเขตเหล่านี้ พวกเขาเพียงแค่บอกคุณว่างานที่ต้องทำเพิ่มขึ้นอย่างไรเมื่อจำนวนอินพุตเพิ่มขึ้น
Abhinav Gauniyal

26

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

O ((n / 2 + 1) * (n / 2)) = O (n 2 /4 + n / 2) = O (n 2 /4) = O (n 2 )

สิ่งนี้ใช้ไม่ได้กับซีรี่ย์ที่ไม่มีที่สิ้นสุด ไม่มีสูตรอาหารเดียวสำหรับกรณีทั่วไปแม้ว่าสำหรับบางกรณีทั่วไปจะใช้ความไม่เท่าเทียมกันดังต่อไปนี้:

O (บันทึกN ) <O ( N ) <O ( NบันทึกN ) <O ( N 2 ) <O ( N k ) <O (e n ) <O ( n !)


8
ย่อม O (N) <O (NlogN)
jk

22

ฉันคิดในแง่ของข้อมูล ปัญหาใด ๆ ประกอบด้วยการเรียนรู้บิตจำนวนหนึ่ง

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

ตัวอย่างเช่นifคำสั่งที่มีสองสาขาซึ่งมีแนวโน้มเท่ากันทั้งสองมีค่าเอนโทรปีของ 1/2 * log (2/1) + 1/2 * log (2/1) = 1/2 * 1 + 1/2 * 1 = 1 ดังนั้นเอนโทรปีของมันคือ 1 บิต

สมมติว่าคุณกำลังค้นหาตารางรายการ N เช่น N = 1024 นั่นเป็นปัญหา 10 บิตเนื่องจาก log (1024) = 10 bits ดังนั้นหากคุณสามารถค้นหาโดยใช้คำสั่ง IF ที่มีแนวโน้มผลลัพธ์เท่ากันก็ควรทำการตัดสินใจ 10 ข้อ

นั่นคือสิ่งที่คุณจะได้รับจากการค้นหาแบบไบนารี

สมมติว่าคุณกำลังค้นหาเชิงเส้น คุณดูที่องค์ประกอบแรกและถามว่ามันเป็นองค์ประกอบที่คุณต้องการ ความน่าจะเป็น 1/1024 ที่เป็นจริงและ 1023/1024 ที่ไม่ใช่ เอนโทรปีของการตัดสินใจนั้นคือ 1/1024 * log (1024/1) + 1023/1024 * log (1024/1023) = 1/1024 * 10 + 1023/1024 * ประมาณ 0 = ประมาณ. 01 บิต คุณได้เรียนรู้น้อยมาก! การตัดสินใจครั้งที่สองนั้นไม่ค่อยดีนัก นั่นคือเหตุผลที่การค้นหาเชิงเส้นช้ามาก ในความเป็นจริงมันเป็นเลขชี้กำลังเป็นจำนวนบิตที่คุณต้องเรียนรู้

สมมติว่าคุณกำลังทำดัชนี สมมติว่าตารางนั้นถูกจัดเรียงไว้ล่วงหน้าในถังขยะจำนวนมากและคุณใช้บิตทั้งหมดในคีย์เพื่อทำดัชนีโดยตรงไปยังรายการตาราง หากมี 1024 ถังขยะเอนโทรปีคือ 1/1024 * บันทึก (1024) + 1/1024 * บันทึก (1024) + ... สำหรับผลลัพธ์ที่เป็นไปได้ทั้งหมด 1024 รายการ นี่คือ 1/1024 * 10 เท่าของผลลัพธ์ 1024 รายการหรือเอนโทรปี 10 บิตสำหรับการดำเนินการสร้างดัชนีหนึ่งรายการ นั่นคือเหตุผลที่การค้นหาการจัดทำดัชนีนั้นรวดเร็ว

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

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

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


ว้าว. คุณมีข้อมูลอ้างอิงที่เป็นประโยชน์เกี่ยวกับเรื่องนี้หรือไม่? ฉันรู้สึกว่าสิ่งนี้มีประโยชน์สำหรับฉันในการออกแบบ / โปรแกรม refactor / debug
Jesvin Jose

3
@ aitchnyu: สำหรับสิ่งที่คุ้มค่าฉันเขียนหนังสือที่ครอบคลุมหัวข้อนั้นและหัวข้ออื่น ๆ มันนานตั้งแต่พิมพ์ออกมา แต่การถ่ายเอกสารจะมีราคาสมเหตุสมผล ฉันพยายามทำให้ GoogleBooks คว้ามัน แต่ในขณะนี้มันยากที่จะเข้าใจว่าใครเป็นผู้มีลิขสิทธิ์
Mike Dunlavey

21

ให้เริ่มต้นจากจุดเริ่มต้น.

ก่อนอื่นยอมรับหลักการที่ว่าการดำเนินการอย่างง่าย ๆ กับข้อมูลสามารถทำได้ในO(1)เวลานั่นคือในเวลาที่ไม่ขึ้นกับขนาดของอินพุต การดำเนินงานดั้งเดิมเหล่านี้ใน C ประกอบด้วย

  1. การดำเนินการทางคณิตศาสตร์ (เช่น + หรือ%)
  2. การดำเนินการเชิงตรรกะ (เช่น &&&)
  3. การดำเนินการเปรียบเทียบ (เช่น <=)
  4. การเข้าถึงโครงสร้างการดำเนินการ (เช่นการทำดัชนีอาร์เรย์เช่น A [i] หรือตัวชี้การชี้ด้วยตัวดำเนินการ ->)
  5. การมอบหมายอย่างง่ายเช่นการคัดลอกค่าลงในตัวแปร
  6. เรียกใช้ฟังก์ชันไลบรารี (เช่น scanf, printf)

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

  1. คำสั่งการกำหนดที่ไม่เกี่ยวข้องกับการเรียกใช้ฟังก์ชันในนิพจน์
  2. อ่านข้อความ
  3. เขียนคำสั่งที่ไม่ต้องการการเรียกใช้ฟังก์ชันเพื่อประเมินข้อโต้แย้ง
  4. คำสั่งกระโดดแบ่ง, ทำต่อ, ข้าม, และส่งคืนนิพจน์โดยที่นิพจน์ไม่มีการเรียกใช้ฟังก์ชัน

ใน C วงฟอร์ลูปจำนวนมากถูกสร้างขึ้นโดยเริ่มต้นตัวแปรดัชนีให้มีค่าบางค่าและเพิ่มค่าตัวแปรนั้น 1 ครั้งในแต่ละรอบลูป for-loop สิ้นสุดลงเมื่อดัชนีถึงขีด จำกัด ตัวอย่างเช่น for-loop

for (i = 0; i < n-1; i++) 
{
    small = i;
    for (j = i+1; j < n; j++)
        if (A[j] < A[small])
            small = j;
    temp = A[small];
    A[small] = A[i];
    A[i] = temp;
}

ใช้ตัวแปรดัชนีฉัน เพิ่มขึ้นทีละ 1 ทุกรอบลูปและการวนซ้ำจะหยุดเมื่อถึง n - 1

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

ยกตัวอย่างเช่น for-loop iterates ((n − 1) − 0)/1 = n − 1 timesเนื่องจาก 0 เป็นค่าเริ่มต้นของ i, n - 1 คือค่าสูงสุดที่เข้าถึงได้โดย i (เช่นเมื่อ i ถึง n − 1, loop จะหยุดและไม่มีการวนซ้ำเกิดขึ้นกับ i = n− 1) และ 1 ถูกเพิ่มเข้ากับ i ในการวนซ้ำแต่ละรอบ

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


ลองพิจารณาตัวอย่างนี้:

(1) for (j = 0; j < n; j++)
(2)   A[i][j] = 0;

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

ในทำนองเดียวกันเราสามารถผูกเวลาทำงานของวงรอบนอกซึ่งประกอบด้วยเส้น (2) ถึง (4) ซึ่งก็คือ

(2) for (i = 0; i < n; i++)
(3)     for (j = 0; j < n; j++)
(4)         A[i][j] = 0;

เราได้กำหนดไว้แล้วว่าการวนรอบของบรรทัด (3) และ (4) ต้องใช้เวลา O (n) ดังนั้นเราสามารถละเลย O (1) เวลาในการเพิ่มค่า i และทดสอบว่า i <n ในการวนซ้ำแต่ละครั้งหรือไม่โดยสรุปว่าการวนซ้ำแต่ละรอบนอกใช้เวลา O (n)

การกำหนดค่าเริ่มต้น i = 0 ของวงนอกและการทดสอบ (n + 1) st ของเงื่อนไข i <n เช่นเดียวกันใช้เวลา O (1) เวลาและสามารถถูกละเลยได้ ในที่สุดเราสังเกตเห็นว่าเราไปรอบ ๆ วงรอบนอก n ครั้งใช้เวลา O (n) สำหรับการวนซ้ำแต่ละครั้งให้O(n^2)เวลาทำงานทั้งหมด


ตัวอย่างการปฏิบัติมากขึ้น

ป้อนคำอธิบายรูปภาพที่นี่


เกิดอะไรขึ้นถ้าคำสั่ง goto มีการเรียกใช้ฟังก์ชันบางอย่างเช่น step3: if (M.step == 3) {M = step3 (เสร็จสิ้น M); } step4: if (M.step == 4) {M = step4 (M); } if (M.step == 5) {M = step5 (M); ข้ามไปขั้นตอนที่ 3; } if (M.step == 6) {M = step6 (M); ข้ามไปขั้นตอนที่ 4; } return cut_matrix (A, M); แล้วจะคำนวณความซับซ้อนอย่างไร มันจะเป็นการเพิ่มหรือการคูณหรือไม่พิจารณา step4 คือ n ^ 3 และ step5 คือ n ^ 2
Taha Tariq

14

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

สิ่งนี้มีข้อดีมากกว่าเพียงแค่ศึกษารหัส สำหรับสิ่งหนึ่งคุณสามารถดูได้ว่าคุณอยู่ในช่วงที่เวลาวิ่งเข้าใกล้ลำดับซีมโทติคหรือไม่ นอกจากนี้คุณอาจพบว่าบางรหัสที่คุณคิดว่าเป็นคำสั่ง O (x) นั้นเป็นคำสั่ง O (x ^ 2) จริง ๆ เช่นเนื่องจากเวลาที่ใช้ในการโทรในไลบรารี


เพียงอัปเดตคำตอบนี้: en.wikipedia.org/wiki/Analysis_of_algorithmsลิงค์นี้มีสูตรที่คุณต้องการ อัลกอริทึมจำนวนมากปฏิบัติตามกฎพลังงานถ้าคุณทำด้วย 2 timepoints และ 2 runtimes บนเครื่องเราสามารถคำนวณความชันบนพล็อตล็อกบันทึก ซึ่งเป็น = log (t2 / t1) / log (n2 / n1) สิ่งนี้ทำให้ฉันมีเลขชี้กำลังสำหรับอัลกอริทึมใน, O (N ^ a) สามารถเปรียบเทียบกับการคำนวณด้วยตนเองโดยใช้รหัส
Christopher John

1
สวัสดีคำตอบที่ดี ฉันสงสัยว่าถ้าคุณทราบเกี่ยวกับห้องสมุดหรือระเบียบวิธี (ฉันทำงานกับ python / R เป็นต้น) เพื่อสรุปวิธีการเชิงประจักษ์นี้ความหมายเช่นการปรับฟังก์ชั่นที่ซับซ้อนต่างๆให้เหมาะสมเพื่อเพิ่มชุดข้อมูลขนาดและหาว่าเกี่ยวข้องกันที่ไหน ขอบคุณ
agenis

10

โดยทั่วไปสิ่งที่ปลูกพืชขึ้น 90% ของเวลาเป็นเพียงการวิเคราะห์ลูป คุณมีลูปซ้อนกันสองชั้นสามเท่าหรือไม่? คุณมีเวลาทำงาน O (n), O (n ^ 2), O (n ^ 3)

ไม่ค่อยมาก (เว้นแต่คุณกำลังเขียนแพลตฟอร์มที่มีไลบรารี่ฐานกว้างขวาง (เช่น. NET BCL หรือ C ++ ของ STL) คุณจะพบกับสิ่งที่ยากกว่าการดูลูปของคุณ (สำหรับคำสั่งในขณะที่ข้ามไป ฯลฯ ... )


1
ขึ้นอยู่กับลูป
kelalaka

8

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

ตอนนี้สร้างต้นไม้ที่สอดคล้องกับอาร์เรย์ทั้งหมดที่คุณทำงานด้วย ที่รูทคุณมีอาเรย์ดั้งเดิมรูทมีลูกสองคนซึ่งเป็น subarrays ทำซ้ำจนกว่าคุณจะมีอาร์เรย์องค์ประกอบเดียวที่ด้านล่าง

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

ดังนั้นเราสามารถ จำกัด ปริมาณงานโดย O (n * log (n))

อย่างไรก็ตาม Big O ซ่อนรายละเอียดบางอย่างซึ่งบางครั้งเราไม่สามารถเพิกเฉยได้ ลองคำนวณลำดับฟีโบนักชีด้วย

a=0;
b=1;
for (i = 0; i <n; i++) {
    tmp = b;
    b = a + b;
    a = tmp;
}

และให้สมมติว่า a และ b เป็น BigIntegers ใน Java หรือสิ่งที่สามารถจัดการกับตัวเลขขนาดใหญ่โดยพลการ คนส่วนใหญ่จะบอกว่านี่เป็นอัลกอริทึม O (n) โดยไม่ต้องสะดุ้ง เหตุผลก็คือคุณมีการวนซ้ำในห่วง for และ O (1) ทำงานข้างลูป

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

1 + 2 + 3 + ... + n = n (n-1) / 2 = O (n ^ 2)

ดังนั้นอัลกอริธึมนี้จึงทำงานในเวลากำลังสอง!


1
คุณไม่ควรสนใจว่าตัวเลขจะถูกเก็บไว้อย่างไรมันก็ไม่ได้เปลี่ยนแปลงว่าอัลกอริทึมจะเติบโตที่ส่วนบนของ O (n)
mikek3332002

8

โดยทั่วไปแล้วฉันคิดว่ามีประโยชน์น้อยกว่า แต่เพื่อความครบถ้วนมีBig Omega Ωซึ่งกำหนดขอบเขตล่างบนความซับซ้อนของอัลกอริทึมและBig Theta Θซึ่งกำหนดทั้งขอบเขตบนและล่าง


7

แยกอัลกอริทึมเป็นชิ้น ๆ ที่คุณรู้จักสัญลักษณ์ O ขนาดใหญ่และรวมเข้ากับโอเปอเรเตอร์ขนาดใหญ่ นั่นเป็นวิธีเดียวที่ฉันรู้

สำหรับข้อมูลเพิ่มเติมตรวจสอบหน้า Wikipediaในหัวข้อ


7

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


6

ในฐานะที่เป็น "วิธีการทำคุณคำนวณ" บิ๊กโอนี้เป็นส่วนหนึ่งของทฤษฎีความซับซ้อนการคำนวณ สำหรับกรณีพิเศษ (หลาย ๆ กรณี) คุณอาจจะมาพร้อมกับฮิวริสติกแบบง่าย ๆ (เช่นการนับการวนซ้ำสำหรับลูปซ้อนกัน) โดยเฉพาะ เมื่อทั้งหมดที่คุณต้องการคือการประมาณขอบเขตบนและคุณไม่คิดว่ามันเป็นแง่ร้ายเกินไป - ซึ่งฉันเดาว่าอาจเป็นคำถามของคุณเกี่ยวกับ

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


6

สำหรับกรณีที่ 1 ภายในวงจะถูกดำเนินการn-iครั้งดังนั้นจำนวนรวมของการประหารชีวิตคือผลรวมสำหรับiไปจาก0ไปn-1(เพราะต่ำกว่าไม่ต่ำกว่าหรือเท่ากับ) n-iของ คุณได้ในที่สุดn*(n + 1) / 2ดังนั้นO(n²/2) = O(n²)ดังนั้น

สำหรับลูปที่ 2 นั้นiอยู่ระหว่าง0และnรวมไว้สำหรับลูปภายนอก จากนั้นวงภายในจะถูกดำเนินการเมื่อjมีค่ามากกว่าnซึ่งเป็นไปไม่ได้อย่างเคร่งครัด


5

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

เป็นตัวอย่างง่ายๆที่บอกว่าคุณต้องการตรวจสติด้วยความเร็วของการเรียงลำดับรายการของ. NET Framework คุณสามารถเขียนสิ่งต่อไปนี้แล้ววิเคราะห์ผลลัพธ์ใน Excel เพื่อให้แน่ใจว่าไม่เกินเส้นโค้ง n * log (n)

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

int nCmp = 0;
System.Random rnd = new System.Random();

// measure the time required to sort a list of n integers
void DoTest(int n)
{
   List<int> lst = new List<int>(n);
   for( int i=0; i<n; i++ )
      lst[i] = rnd.Next(0,1000);

   // as we sort, keep track of the number of comparisons performed!
   nCmp = 0;
   lst.Sort( delegate( int a, int b ) { nCmp++; return (a<b)?-1:((a>b)?1:0)); }

   System.Console.Writeline( "{0},{1}", n, nCmp );
}


// Perform measurement for a variety of sample sizes.
// It would be prudent to check multiple random samples of each size, but this is OK for a quick sanity check
for( int n = 0; n<1000; n++ )
   DoTest(n);

4

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

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

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


4

สิ่งที่มักถูกมองข้ามคือพฤติกรรมที่คาดหวังของอัลกอริทึมของคุณ ไม่เปลี่ยน Big-O ของอัลกอริทึมของคุณแต่จะเกี่ยวข้องกับคำสั่ง "การปรับให้เหมาะสมก่อนวัยอันควร.."

พฤติกรรมที่คาดหวังของอัลกอริทึมของคุณคือ - โง่มาก - ความเร็วที่คุณคาดหวังว่าอัลกอริทึมของคุณจะทำงานกับข้อมูลที่คุณน่าจะเห็นมากที่สุด

ตัวอย่างเช่นหากคุณกำลังค้นหาค่าในรายการมันเป็น O (n) แต่ถ้าคุณรู้ว่ารายการส่วนใหญ่ที่คุณเห็นจะมีมูลค่าของคุณอยู่ข้างหน้าพฤติกรรมทั่วไปของอัลกอริทึมของคุณจะเร็วขึ้น

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


4

คำถามที่ดี!

คำเตือน: คำตอบนี้มีคำเท็จดูความคิดเห็นด้านล่าง

หากคุณใช้ Big O คุณกำลังพูดถึงกรณีที่แย่กว่านั้น (เพิ่มเติมเกี่ยวกับความหมายในภายหลัง) นอกจากนี้ยังมีทุน theta สำหรับกรณีทั่วไปและโอเมก้าขนาดใหญ่สำหรับกรณีที่ดีที่สุด

ลองชมไซต์นี้เพื่อดูคำจำกัดความที่เป็นทางการของ Big O: https://xlinux.nist.gov/dads/HTML/bigOnotation.html

f (n) = O (g (n)) หมายถึงมีค่าคงที่เป็นบวก c และ k เช่นที่ 0 ≤ f (n) ≤ cg (n) สำหรับทุก n ≥ k ค่าของ c และ k ต้องได้รับการแก้ไขสำหรับฟังก์ชัน f และต้องไม่ขึ้นอยู่กับ n


ตกลงดังนั้นตอนนี้เราหมายถึงอะไรโดยความซับซ้อน "กรณีที่ดีที่สุด" และ "กรณีที่เลวร้ายที่สุด"?

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

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

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


1
สิ่งนี้ไม่ถูกต้อง Big O หมายถึง "ขอบเขตบน" ไม่ใช่กรณีที่เลวร้ายที่สุด
Samy Bencherif

1
มันเป็นความเข้าใจผิดร่วมกันว่า big-O หมายถึงกรณีที่เลวร้ายที่สุด O และΩเกี่ยวข้องกับกรณีที่เลวร้ายที่สุดและดีที่สุดได้อย่างไร?
Bernhard Barker

1
นี่เป็นสิ่งที่ทำให้เข้าใจผิด Big-O หมายถึงขอบเขตบนของฟังก์ชัน f (n) Omega หมายถึงขอบเขตล่างของฟังก์ชัน f (n) มันไม่ได้เกี่ยวข้องกับกรณีที่ดีที่สุดหรือกรณีที่เลวร้ายที่สุด
Tasneem Haider

1
คุณสามารถใช้ Big-O เป็นขอบเขตบนสำหรับกรณีที่ดีที่สุดหรือเลวร้ายที่สุด แต่นอกเหนือจากนั้นใช่ไม่มีความสัมพันธ์
Samy Bencherif

2

ฉันไม่รู้วิธีแก้ปัญหานี้โดยทางโปรแกรม แต่สิ่งแรกที่คนทำคือเราสุ่มตัวอย่างอัลกอริทึมสำหรับจำนวนรูปแบบการดำเนินการเสร็จแล้วพูด 4n ^ 2 + 2n +1 เรามีกฎ 2 ข้อ:

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

หากเราลดความซับซ้อนของ f (x) โดยที่ f (x) เป็นสูตรสำหรับการดำเนินการที่ทำได้ (4n ^ 2 + 2n + 1 อธิบายไว้ด้านบน) เราจะได้รับค่า big-O [O (n ^ 2) ในสิ่งนี้ กรณี]. แต่สิ่งนี้จะต้องคำนึงถึงการแก้ไข Lagrange ในโปรแกรมซึ่งอาจใช้งานยาก และถ้าค่า big-O ที่แท้จริงคือ O (2 ^ n) และเราอาจมีบางอย่างเช่น O (x ^ n) ดังนั้นอัลกอริทึมนี้อาจจะไม่สามารถตั้งโปรแกรมได้ แต่ถ้ามีคนพิสูจน์ฉันผิดให้รหัสฉัน . . .


2

สำหรับรหัส A วงรอบนอกจะดำเนินการเป็นn+1ครั้ง '1' หมายถึงกระบวนการที่ตรวจสอบว่าฉันยังคงเป็นไปตามข้อกำหนดหรือไม่ และห่วงภายในวิ่งnครั้งn-2ครั้ง .... 0+2+..+(n-2)+n= (0+n)(n+1)/2= O(n²)ดังนั้น

สำหรับรหัส B แม้ว่าวงในจะไม่เข้ามาและเรียกใช้งาน foo () วงในก็จะถูกดำเนินการสำหรับ n ครั้งขึ้นอยู่กับเวลาในการดำเนินการลูปภายนอกซึ่งเป็น O (n)


1

ฉันต้องการอธิบาย Big-O ในมุมมองที่ต่างออกไปเล็กน้อย

Big-O เป็นเพียงการเปรียบเทียบความซับซ้อนของโปรแกรมซึ่งหมายถึงความเร็วที่เพิ่มขึ้นเมื่ออินพุทเพิ่มขึ้นไม่ใช่เวลาที่แน่นอนที่ใช้ในการดำเนินการ

IMHO ในสูตรบิ๊กโอคุณไม่ควรใช้สมการที่ซับซ้อนมากขึ้น (คุณอาจติดกับสูตรในกราฟต่อไปนี้) อย่างไรก็ตามคุณยังอาจใช้สูตรที่แม่นยำกว่านี้ (เช่น 3 ^ n, n ^ 3, .. .) แต่ยิ่งไปกว่านั้นบางครั้งก็อาจทำให้เข้าใจผิด! ดังนั้นดีกว่าที่จะให้มันง่ายที่สุด

ป้อนคำอธิบายรูปภาพที่นี่

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

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