คุณคำนวณ log ฐาน 2 ใน Java สำหรับจำนวนเต็มได้อย่างไร


138

ฉันใช้ฟังก์ชันต่อไปนี้เพื่อคำนวณ log ฐาน 2 สำหรับจำนวนเต็ม:

public static int log2(int n){
    if(n <= 0) throw new IllegalArgumentException();
    return 31 - Integer.numberOfLeadingZeros(n);
}

มันมีประสิทธิภาพที่ดีที่สุด?

มีคนรู้ว่าฟังก์ชั่น J2SE API พร้อมสำหรับวัตถุประสงค์นั้นหรือไม่?

UPD1 น่าแปลกใจสำหรับฉันเลขคณิตจุดลอยตัวดูเหมือนจะเร็วกว่าเลขคณิตจำนวนเต็ม

UPD2 เนื่องจากความคิดเห็นฉันจะดำเนินการตรวจสอบอย่างละเอียดมากขึ้น

UPD3 ฟังก์ชันเลขจำนวนเต็มของฉันเร็วกว่า Math.log (n) / Math.log (2) 10 เท่า


1
คุณทดสอบประสิทธิภาพของสิ่งนี้ได้อย่างไร ในระบบของฉัน (Core i7, jdk 1.6 x64) เวอร์ชันจำนวนเต็มเร็วกว่ารุ่นเลขทศนิยมเกือบ 10 เท่า อย่าลืมทำบางสิ่งบางอย่างกับผลลัพธ์ของฟังก์ชันเพื่อให้ JIT ไม่สามารถลบการคำนวณทั้งหมดได้!
x4u

คุณถูก. ฉันไม่ได้ใช้ผลลัพธ์ของการคำนวณและคอมไพเลอร์ได้ปรับปรุงบางสิ่งบางอย่าง ตอนนี้ฉันมีผลลัพธ์เช่นเดียวกับคุณ - ฟังก์ชั่นจำนวนเต็มเร็วขึ้น 10 เท่า (Core 2 Duo, jdk 1.6 c64)
Nulldevice

6
สิ่งนี้จะช่วยให้คุณได้อย่างมีประสิทธิภาพMath.floor(Math.log(n)/Math.log(2))ดังนั้นมันจึงไม่คำนวณฐานบันทึก 2 จริง ๆ !
Dori

คำตอบ:


74

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

ฉันมักจะพยายามหลีกเลี่ยงการคำนวณ FP ทุกครั้งที่ทำได้

การดำเนินการจุดลอยตัวไม่ถูกต้อง คุณไม่มีทางรู้แน่นอนว่าจะ(int)(Math.log(65536)/Math.log(2))ประเมินอะไร ตัวอย่างเช่นMath.ceil(Math.log(1<<29) / Math.log(2))เป็น 30 บนพีซีของฉันซึ่งในทางคณิตศาสตร์มันควรจะเป็น 29 ฉันไม่พบค่าสำหรับ x ที่(int)(Math.log(x)/Math.log(2))ล้มเหลว (เพียงเพราะมีเพียง 32 "อันตราย" ค่า) แต่มันไม่ได้หมายความว่ามันจะทำงาน เช่นเดียวกันกับพีซีเครื่องใดก็ได้

เคล็ดลับปกติที่นี่ใช้ "epsilon" เมื่อปัดเศษ ชอบ(int)(Math.log(x)/Math.log(2)+1e-10)ไม่ควรล้มเหลว การเลือก "เอปไซลอน" นี้ไม่ใช่งานที่ไม่สำคัญ

การสาธิตมากขึ้นโดยใช้งานทั่วไปที่มากขึ้น - พยายามนำไปใช้int log(int x, int base):

รหัสการทดสอบ:

static int pow(int base, int power) {
    int result = 1;
    for (int i = 0; i < power; i++)
        result *= base;
    return result;
}

private static void test(int base, int pow) {
    int x = pow(base, pow);
    if (pow != log(x, base))
        System.out.println(String.format("error at %d^%d", base, pow));
    if(pow!=0 && (pow-1) != log(x-1, base))
        System.out.println(String.format("error at %d^%d-1", base, pow));
}

public static void main(String[] args) {
    for (int base = 2; base < 500; base++) {
        int maxPow = (int) (Math.log(Integer.MAX_VALUE) / Math.log(base));
        for (int pow = 0; pow <= maxPow; pow++) {
            test(base, pow);
        }
    }
}

หากเราใช้ลอการิทึมที่นำมาใช้โดยตรง

static int log(int x, int base)
{
    return (int) (Math.log(x) / Math.log(base));
}

พิมพ์นี้:

error at 3^5
error at 3^10
error at 3^13
error at 3^15
error at 3^17
error at 9^5
error at 10^3
error at 10^6
error at 10^9
error at 11^7
error at 12^7
...

เพื่อกำจัดข้อผิดพลาดอย่างสมบูรณ์ฉันต้องเพิ่ม epsilon ซึ่งอยู่ระหว่าง 1e-11 และ 1e-14 คุณช่วยบอกเรื่องนี้ก่อนการทดสอบได้ไหม ฉันไม่สามารถทำได้อย่างแน่นอน


3
"มันไม่ได้หมายความว่ามันจะทำงานในลักษณะเดียวกันบนพีซีเครื่องใดก็ได้" - ถ้าคุณใช้มันจะstrictfpไม่ใช่หรือ
Ken

@Ken: อาจจะ ... แต่คุณสามารถมั่นใจได้หลังจากระบุค่าอินพุตที่เป็นไปได้ทั้งหมดเท่านั้น (เราโชคดีที่มีอยู่ไม่กี่คนที่นี่)
Rotsor

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

วิธีการเกี่ยวกับreturn ((long)Math.log(x) / (long)Math.log(base));การแก้ไขข้อผิดพลาดทั้งหมดหรือไม่
ไม่ใช่ข้อผิดพลาด

92

นี่คือฟังก์ชั่นที่ฉันใช้สำหรับการคำนวณนี้:

public static int binlog( int bits ) // returns 0 for bits=0
{
    int log = 0;
    if( ( bits & 0xffff0000 ) != 0 ) { bits >>>= 16; log = 16; }
    if( bits >= 256 ) { bits >>>= 8; log += 8; }
    if( bits >= 16  ) { bits >>>= 4; log += 4; }
    if( bits >= 4   ) { bits >>>= 2; log += 2; }
    return log + ( bits >>> 1 );
}

มันเร็วกว่า Integer.numberOfLeadingZeros () (20-30%) และเร็วขึ้นเกือบ 10 เท่า (jdk 1.6 x64) กว่าการใช้งานแบบ Math.log () เช่นนี้:

private static final double log2div = 1.000000000001 / Math.log( 2 );
public static int log2fp0( int bits )
{
    if( bits == 0 )
        return 0; // or throw exception
    return (int) ( Math.log( bits & 0xffffffffL ) * log2div );
}

ฟังก์ชั่นทั้งสองส่งคืนผลลัพธ์เดียวกันสำหรับค่าอินพุตที่เป็นไปได้ทั้งหมด

อัปเดต: เซิร์ฟเวอร์ Java 1.7 JIT สามารถแทนที่ฟังก์ชันคณิตศาสตร์แบบสแตติกสองสามตัวด้วยการใช้งานทางเลือกโดยยึดตาม CPU ภายใน หนึ่งในฟังก์ชั่นเหล่านั้นคือ Integer.numberOfLeadingZeros () ดังนั้นด้วย VM ที่ใหม่กว่าหรือ 1.7 เซิร์ฟเวอร์การใช้งานอย่างหนึ่งในคำถามนั้นเร็วกว่าที่binlogกล่าวไว้เล็กน้อย น่าเสียดายที่ลูกค้า JIT ดูเหมือนจะไม่มีการเพิ่มประสิทธิภาพนี้

public static int log2nlz( int bits )
{
    if( bits == 0 )
        return 0; // or throw exception
    return 31 - Integer.numberOfLeadingZeros( bits );
}

การใช้งานนี้ยังส่งคืนผลลัพธ์เดียวกันสำหรับค่าอินพุตที่เป็นไปได้ทั้งหมด 2 ^ 32 เช่นเดียวกับการใช้งานอีกสองรายการที่ฉันโพสต์ด้านบน

นี่คือ runtimes จริงบนพีซีของฉัน (Sandy Bridge i7):

ลูกค้า JDK 1.7 32 บิตบิต VM:

binlog:         11.5s
log2nlz:        16.5s
log2fp:        118.1s
log(x)/log(2): 165.0s

เซิร์ฟเวอร์ JDK 1.7 x64 VM:

binlog:          5.8s
log2nlz:         5.1s
log2fp:         89.5s
log(x)/log(2): 108.1s

นี่คือรหัสทดสอบ:

int sum = 0, x = 0;
long time = System.nanoTime();
do sum += log2nlz( x ); while( ++x != 0 );
time = System.nanoTime() - time;
System.out.println( "time=" + time / 1000000L / 1000.0 + "s -> " + sum );

9
BSRคำสั่งของ x86 ทำ32 - numberOfLeadingZerosแต่ไม่ได้กำหนดไว้สำหรับ 0 ดังนั้นคอมไพเลอร์ (JIT) ต้องตรวจสอบว่าไม่ใช่ศูนย์หากไม่สามารถพิสูจน์ได้ว่ามันไม่จำเป็นต้อง แนะนำชุดส่วนขยายชุดคำสั่ง BMI (แฮสและรุ่นใหม่กว่า)LZCNTซึ่งใช้อย่างสมบูรณ์numberOfLeadingZerosในการเรียนการสอนเพียงครั้งเดียว ทั้งสองมีความหน่วงแฝงอยู่ 3 รอบ, 1 ต่อรอบการรับส่งข้อมูล ดังนั้นฉันขอแนะนำให้ใช้อย่างแน่นอนnumberOfLeadingZerosเพราะนั่นทำให้ง่ายสำหรับ JVM ที่ดี (สิ่งที่แปลกหนึ่งที่เกี่ยวกับlzcntว่ามีการอ้างอิงที่ผิดพลาดในค่าเดิมของมันเขียนทับลงทะเบียน.)
ปีเตอร์ Cordes

ฉันสนใจมากที่สุดในความคิดเห็นของคุณเกี่ยวกับการเปลี่ยน JIT CPU ภายในของ Java 1.7 เซิร์ฟเวอร์ คุณมี URL อ้างอิงหรือไม่? (ลิงค์ซอร์สโค้ดของ JIT ก็โอเคเช่นกัน)
kevinarpe

37

ลอง Math.log(x) / Math.log(2)


8
ในขณะที่วิชาคณิตศาสตร์นี้ถูกต้องโปรดทราบว่ามีความเสี่ยงของการคำนวณผิดพลาดเนื่องจากการคำนวณเลขทศนิยมไม่แม่นยำตามที่อธิบายไว้ในคำตอบของ Rotsor
leeyuiwah

28

คุณสามารถใช้ตัวตน

            log[a]x
 log[b]x = ---------
            log[a]b

ดังนั้นสิ่งนี้จะใช้ได้สำหรับ log2

            log[10]x
 log[2]x = ----------
            log[10]2

เพียงแค่เสียบเข้ากับวิธีการทางคณิตศาสตร์ java log10 ....

http://mathforum.org/library/drmath/view/55565.html


3
ในขณะที่วิชาคณิตศาสตร์นี้ถูกต้องโปรดทราบว่ามีความเสี่ยงของการคำนวณผิดพลาดเนื่องจากการคำนวณเลขทศนิยมไม่แม่นยำตามที่อธิบายไว้ในคำตอบของ Rotsor
leeyuiwah

18

ทำไมจะไม่ล่ะ:

public static double log2(int n)
{
    return (Math.log(n) / Math.log(2));
}

6
ในขณะที่วิชาคณิตศาสตร์นี้ถูกต้องโปรดทราบว่ามีความเสี่ยงของการคำนวณผิดพลาดเนื่องจากการคำนวณเลขทศนิยมไม่แม่นยำตามที่อธิบายไว้ในคำตอบของ Rotsor
leeyuiwah

9

มีฟังก์ชั่นในไลบรารีฝรั่ง:

LongMath.log2()

ดังนั้นฉันแนะนำให้ใช้


ฉันจะเพิ่มแพ็คเกจนี้ลงในใบสมัครของฉันได้อย่างไร
Elvin Mammadov

ดาวน์โหลด jar จากที่นี่และเพิ่มไปยังเส้นทางการสร้างของโครงการ
Debosmit Ray

2
ฉันควรเพิ่มไลบรารี่ในแอปพลิเคชันของฉันเพื่อใช้งานฟังก์ชั่นเดียวหรือไม่?
Tash Pemhiwa

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

3

เพื่อเพิ่มคำตอบ x4u ซึ่งให้ข้อมูลพื้นฐานของบันทึกเลขฐานสองของคุณฟังก์ชั่นนี้จะคืนค่าเพดานของบันทึกเลขฐานสองของตัวเลข:

public static int ceilbinlog(int number) // returns 0 for bits=0
{
    int log = 0;
    int bits = number;
    if ((bits & 0xffff0000) != 0) {
        bits >>>= 16;
        log = 16;
    }
    if (bits >= 256) {
        bits >>>= 8;
        log += 8;
    }
    if (bits >= 16) {
        bits >>>= 4;
        log += 4;
    }
    if (bits >= 4) {
        bits >>>= 2;
        log += 2;
    }
    if (1 << log < number)
        log++;
    return log + (bits >>> 1);
}

ตัวแปร "หมายเลข" อยู่ที่ไหน?
barteks2x


0

มาเพิ่ม:

int[] fastLogs;

private void populateFastLogs(int length) {
    fastLogs = new int[length + 1];
    int counter = 0;
    int log = 0;
    int num = 1;
    fastLogs[0] = 0;
    for (int i = 1; i < fastLogs.length; i++) {
        counter++;
        fastLogs[i] = log;
        if (counter == num) {
            log++;
            num *= 2;
            counter = 0;
        }
    }
}

ที่มา: https://github.com/pochuan/cs166/blob/master/ps1/rmq/SparseTableRMQ.java


นั่นจะเป็นการสร้างตารางการค้นหา OP ขอวิธีที่เร็วกว่าในการ "คำนวณ" ลอการิทึม
เดฟ

-4

ในการคำนวณล็อกฐาน 2 จาก n สามารถใช้นิพจน์ต่อไปนี้:

double res = log10(n)/log10(2);

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