PLSQL计算质数
看到有人实现了一个计算质数的函数,就是效率有点差,贴一个以前写的计算质数的算法。 目标很简单,列出100以内的质数。 其实算法很简单,两个循环就搞定了。但是发现使用不同的算法,执行效率差别之大相当惊人,特别是数据量级很大的时候。 下面就是最常见的一种写法:(也是最差的一种) [PHP] SQL> SET SERVEROUT ON SQL> DECLARE 2 V_FLAG BOOLEAN; 3 BEGIN 4 FOR I IN 2 .. 100 LOOP 5 V_FLAG := TRUE; 6 FOR J IN 2 .. I - 1 LOOP 7 IF MOD(I,J) = 0 THEN 8 V_FLAG := FALSE; 9 END IF; 10 END LOOP; 11 12 IF V_FLAG = TRUE THEN 13 DBMS_OUTPUT.PUT_LINE(I); 14 END IF; 15 END LOOP; 16 END; 17 / 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 PL/SQL 过程已成功完成。 已用时间: 00: 00: 00.09 . [/PHP] 由于屏幕输出操作比较慢,为了避免影响,将屏幕输出关闭。并将数据量增大到100000,以下所有的测试都在这个相同条件下进行。 [PHP] SQL> DECLARE 2 V_FLAG BOOLEAN; 3 BEGIN 4 FOR I IN 2 .. 100000 LOOP 5 V_FLAG := TRUE; 6 FOR J IN 2 .. I - 1 LOOP 7 IF MOD(I,J) = 0 THEN 8 V_FLAG := FALSE; 9 END IF; 10 END LOOP; 11 12 IF V_FLAG = TRUE THEN 13 --DBMS_OUTPUT.PUT_LINE(I); 14 NULL; 15 END IF; 16 END LOOP; 17 END; 18 / PL/SQL 过程已成功完成。 已用时间: 02: 02: 58.73 . [/PHP] 这种方法在100000数据量的用时居然达到了2个小时。 如果稍微仔细考虑一下,就会发现,系统做了很多没有必要的工作,首先判断是否能整除的时候不需要循环到I – 1,只要执行到I的平方根就可以了,而且,如果I可以被整除就不需要继续循环,可以马上跳出内层循环了。经过简单优化后: [PHP] SQL> DECLARE 2 V_FLAG BOOLEAN; 3 BEGIN 4 FOR I IN 2 .. 100000 LOOP 5 V_FLAG := TRUE; 6 FOR J IN 2 .. TRUNC(POWER(I, 0.5)) LOOP 7 IF MOD(I,J) = 0 THEN 8 V_FLAG := FALSE; 9 EXIT; 10 END IF; 11 END LOOP; 12 13 IF V_FLAG = TRUE THEN 14 --DBMS_OUTPUT.PUT_LINE(I); 15 NULL; 16 END IF; 17 END LOOP; 18 END; 19 / PL/SQL 过程已成功完成。 已用时间: 00: 00: 16.21 . [/PHP] 效果十分的明显,从2个多小时,缩减到了16秒。 算法还可以进一步优化,考虑到质数中只有2是偶数,其他都是奇数,可以将2单独处理,然后将循环的步长设置为2,这样外层循环次数就减少了一半。 [PHP] SQL> DECLARE 2 I NUMBER DEFAULT 3; 3 V_FLAG BOOLEAN; 4 BEGIN 5 --DBMS_OUTPUT.PUT_LINE(2); 6 WHILE I < 100000 LOOP 7 V_FLAG := TRUE; 8 FOR J IN 2 .. TRUNC(POWER(I, 0.5)) LOOP 9 IF MOD(I,J) = 0 THEN 10 V_FLAG := FALSE; 11 EXIT; 12 END IF; 13 END LOOP; 14 15 IF V_FLAG = TRUE THEN 16 --DBMS_OUTPUT.PUT_LINE(I); 17 NULL; 18 END IF; 19 I := I + 2; 20 END LOOP; 21 END; 22 / PL/SQL 过程已成功完成。 已用时间: 00: 00: 09.37 . [/PHP] 仔细考虑一下,其实用来被整除的数是质数就足够了,不需要对所有奇数进行判断。在下面的过程中,使用索引表来保存计算得到的所有的质数: [PHP] SQL> DECLARE 2 TYPE T_RECORD IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; 3 V_RESULT T_RECORD; 4 V_FLAG BOOLEAN; 5 V_CNT NUMBER; 6 I NUMBER DEFAULT 3; 7 BEGIN 8 V_RESULT(1) := 2; 9 --DBMS_OUTPUT.PUT_LINE(V_RESULT(1)); 10 WHILE(I < 100000) LOOP 11 V_FLAG := TRUE; 12 V_CNT := V_RESULT.COUNT; 13 FOR J IN 1..V_CNT LOOP 14 IF V_RESULT(J) > POWER(I, 0.5) THEN 15 EXIT; 16 END IF; 17 IF MOD(I,V_RESULT(J)) = 0 THEN 18 V_FLAG := FALSE; 19 EXIT; 20 END IF; 21 END LOOP; 22 IF V_FLAG THEN 23 -- DBMS_OUTPUT.PUT_LINE(I); 24 V_RESULT(V_CNT+1) := I; 25 END IF; 26 I := I + 2; 27 END LOOP; 28 END; 29 / PL/SQL 过程已成功完成。 已用时间: 00: 00: 06.68 . [/PHP] 已经将速度提高到了6秒左右,还能不能更快呢?注意到在最内层循环中调用了一个函数POWER(I, 0.5),下面将这个表达式转换一下,避免使用这个函数: [PHP] SQL> DECLARE 2 TYPE T_RECORD IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; 3 V_RESULT T_RECORD; 4 V_FLAG BOOLEAN; 5 I NUMBER DEFAULT 3; 6 BEGIN 7 --DBMS_OUTPUT.PUT_LINE(2); 8 V_RESULT(1) := 2; 9 WHILE(I < 100000) LOOP 10 V_FLAG := TRUE; 11 FOR J IN 1..V_RESULT.COUNT LOOP 12 IF V_RESULT(J) * V_RESULT(J) > I THEN 13 EXIT; 14 END IF; 15 IF MOD(I,V_RESULT(J)) = 0 THEN 16 V_FLAG := FALSE; 17 EXIT; 18 END IF; 19 END LOOP; 20 IF V_FLAG THEN 21 -- DBMS_OUTPUT.PUT_LINE(I); 22 V_RESULT(V_RESULT.COUNT + 1) := I; 23 END IF; 24 I := I + 2; 25 END LOOP; 26 END; 27 / PL/SQL 过程已成功完成。 已用时间: 00: 00: 01.03 . [/PHP] 难以置信吧,一个执行两个小时的PL/SQL,通过算法的调整可以优化到了1秒。 其实能优化的地方还有很多,不过这些优化能带来的性能提升已经很小了。比如: [PHP] SQL> DECLARE 2 TYPE T_RECORD IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; 3 V_RESULT T_RECORD; 4 V_FLAG BOOLEAN; 5 I NUMBER DEFAULT 3; 6 BEGIN 7 --DBMS_OUTPUT.PUT_LINE(2); 8 V_RESULT(0) := 2; 9 WHILE(I < 100000) LOOP 10 V_FLAG := TRUE; 11 FOR J IN 1..V_RESULT.COUNT - 1 LOOP 12 IF V_RESULT(J) * V_RESULT(J) > I THEN 13 EXIT; 14 END IF; 15 IF MOD(I,V_RESULT(J)) = 0 THEN 16 V_FLAG := FALSE; 17 EXIT; 18 END IF; 19 END LOOP; 20 IF V_FLAG THEN 21 -- DBMS_OUTPUT.PUT_LINE(I); 22 V_RESULT(V_RESULT.COUNT) := I; 23 END IF; 24 I := I + 2; 25 END LOOP; 26 END; 27 / PL/SQL 过程已成功完成。 已用时间: 00: 00: 00.96 . [/PHP] 由于从3开始步长为2,因此判断随后的质数的时候,没有必要用2去整除,而直接可以从3开始。 过程仍然可以进一步优化,可以省略掉不必要的赋值和判断语句: [PHP] SQL> DECLARE 2 TYPE T_RECORD IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; 3 V_RESULT T_RECORD; 4 I NUMBER DEFAULT 3; 5 BEGIN 6 --DBMS_OUTPUT.PUT_LINE(2); 7 V_RESULT(1) := 3; 8 WHILE(I < 100000) LOOP 9 FOR J IN 1..V_RESULT.COUNT LOOP 10 IF MOD(I,V_RESULT(J)) = 0 THEN 11 EXIT; 12 END IF; 13 IF V_RESULT(J) * V_RESULT(J) > I THEN 14 -- DBMS_OUTPUT.PUT_LINE(I); 15 V_RESULT(V_RESULT.COUNT + 1) := I; 16 EXIT; 17 END IF; 18 END LOOP; 19 I := I + 2; 20 END LOOP; 21 V_RESULT(0) := 2; 22 END; 23 / PL/SQL 过程已成功完成。 已用时间: 00: 00: 00.96 . [/PHP] 但是在100000这个数量级已经看不出性能的差别了。正如Tom所说的,系统总是可以提高1%的性能,不过付出的代价会越来越大。 刚才测试发现,将MOD函数转换一下,性能还会有一个相对明显的提升 [PHP] SQL> DECLARE 2 TYPE T_RECORD IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; 3 V_RESULT T_RECORD; 4 I NUMBER DEFAULT 3; 5 BEGIN 6 --DBMS_OUTPUT.PUT_LINE(2); 7 V_RESULT(1) := 3; 8 WHILE(I < 100000) LOOP 9 FOR J IN 1..V_RESULT.COUNT LOOP 10 IF V_RESULT(J) * V_RESULT(J) > I THEN 11 --DBMS_OUTPUT.PUT_LINE(I); 12 V_RESULT(V_RESULT.COUNT + 1) := I; 13 EXIT; 14 END IF; 15 IF TRUNC(I/V_RESULT(J)) = I/V_RESULT(J) THEN 16 EXIT; 17 END IF; 18 END LOOP; 19 I := I + 2; 20 END LOOP; 21 V_RESULT(0) := 2; 22 END; 23 / PL/SQL 过程已成功完成。 已用时间: 00: 00: 00.87 . [/PHP] 再来一次尝试: [PHP] SQL> DECLARE 2 TYPE T_RECORD IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; 3 V_RESULT T_RECORD; 4 I NUMBER DEFAULT 3; 5 N NUMBER DEFAULT 0; 6 BEGIN 7 --DBMS_OUTPUT.PUT_LINE(2); 8 V_RESULT(1) := 3; 9 WHILE(I < 100000) LOOP 10 FOR J IN 1..V_RESULT.COUNT LOOP 11 IF V_RESULT(J) * V_RESULT(J) > I THEN 12 --DBMS_OUTPUT.PUT_LINE(I); 13 V_RESULT(V_RESULT.COUNT + 1) := I; 14 EXIT; 15 END IF; 16 IF TRUNC(I/V_RESULT(J)) = I/V_RESULT(J) THEN 17 EXIT; 18 END IF; 19 END LOOP; 20 IF N = 2 THEN 21 I := I + 4; 22 N := 1; 23 ELSE 24 I := I + 2; 25 N := N + 1; 26 END IF; 27 END LOOP; 28 V_RESULT(0) := 2; 29 END; 30 / PL/SQL 过程已成功完成。 已用时间: 00: 00: 00.84 . [/PHP] 看来Tom说的确实没有错,优化的方法总是存在的。 原文出自:http://space.itpub.net/4227/viewspace-68767 |