演算法|隨機 因為教學的需要,我決定編寫一個asp+ms sql2000的網上考試系統,其功能主要為:實現判斷題、單項多項選擇題和填空題的線上自動答題、改卷;並將學生的錯誤答案記入資料庫,供教師分析。在編寫從題庫中隨機抽取試題這一模組的演算法上,卻頗費了一番周折,現將解決過程記錄如下,以供大家參考。
為了便於說明問題,文中提供的代碼中的變數pd為從題庫中要抽取出來考試的試題數量,資料庫表名與欄位名我都使用了中文,並僅以判斷題為例。
演算法一
由於不知道如何?從題庫中隨機抽取試題的sql語句,我在網上下載了幾個免費的考試系統進行研究,找到了第一種演算法,其思路為先將資料庫中所有資料讀出,獲得試題的總數後,產生一個1~(試題的總數-考試的試題數量)之間的隨機數,然後從這裡開始讀出資料:
<% set rs=server.CreateObject("ADODB.RecordSet")
sql="select * from 判斷題 order by id asc"
rs.open sql,conn,1,1
mycound=rs.Recordcount '取得試題總數
randomize '初始化隨機數種子值
n=fix((mycound-pd+1)*Rnd+1)
rs.move n ‘指標移到n這個隨機數這個位置
for i=1 to pd
session("pdda")=session("pdda")&rs("正確答案")&"|" ‘用session來記錄標準答案
‘輸出試題及答案%>
<tr>
<td width="10%" ><%=i%>、<%=rs("題目內容")%></td>
<td align="center" width="10%" ><select name="cate<%=i%>">
<option selected value=True>對</option>
<option value=False>錯</option></select> </td>
</tr>
<% rs.movenext
next
rs.close%>
這種演算法基本上可以實現隨機抽取試題,並讓每個學生的試題和每一次重新整理以後的試題都不相同,但是它的最大不足在於試題的先後順序總是相同,特別是題庫中試題不多的時候,學生幾乎可以用背答案方法來應付考試了。雖然可以通過改變資料的排序方式來改變試題的先後順序,但變化總是不大。
演算法二
第二種演算法的思路很簡單,就是不斷產生1~題庫中的試題總數之間的隨機數,然後到資料庫中讀取這條記錄,直到滿足考試的試題量為止。
<%
set rs=server.CreateObject("ADODB.RecordSet")
sql="select * from 判斷題 order by id asc"
rs.open sql,conn,1,1
mycound=rs.Recordcount '取得題庫中的試題總數
rs.close
for i=1 to pd
randomize
sid=int((mycound +1)*rnd+1) ‘產生1~題庫中的試題總數之間的隨機數
set rs=conn.execute("select * from判斷題where id="&sid)
while rs.eof
randomize
sid=int((mycound +1)*rnd+1)
set rs=conn.execute("select * from判斷題where id="&sid) ‘如果資料庫中找不到這條試題,就繼續產生隨機數讀取試題。
wend
session("pdda")=session("pdda")&rs("正確答案")&"|" ‘用session來記錄標準答案
‘輸出試題及答案%>
<tr>
<td width="10%" ><%=i%>、<%=rs("題目內容")%></td>
<td align="center" width="10%" ><select name="cate<%=i%>">
<option selected value=True>對</option>
<option value=False>錯</option></select> </td>
</tr>
<%
next
%>
這種演算法應該是真正意義上的隨機抽取試題,但是遺憾的是如果在題庫中題量不多的情況下,很容易會在資料庫中讀取重複的試題,如果再使用一個變數來儲存已經讀取過的試題id來解決試題重複的問題,演算法就過於繁瑣,是很不可取的。
演算法二補充:
第二種演算法的思路很簡單,就是不斷產生1~題庫中的試題總數之間的隨機數,然後到資料庫中讀取這條記錄,直到滿足考試的試題量為止。當時我認為這種演算法應該是真正意義上的隨機抽取試題,但是遺憾的是如果在題庫中題量不多的情況下,很容易會在資料庫中讀取重複的試題,雖然也可以再使用一個變數或數組來儲存已經讀取過的試題id來解決試題重複的問題,演算法就過於繁瑣。為此,我片面地認為不可取的。其實用一個變數或數組來儲存已經讀取過的試題id,在演算法上並不繁瑣。
<%
寫一個產生隨機記錄的函數
Function rndtest(m_count,r_count) ''參數m_count為試題總數,r_count為要讀出的試題數
dim x,st,i
i=0
do while i>=r_count
randomize
x=fix(rnd*m_count)+1 ''產生1~m_count的隨機數
if not instr(st,x)>0 then
st=st&x&"," ''用,分割
i=i+1
end if
if i>=m_count then exit do ''如果m_count小於r_count將出現死迴圈,於是判斷並跳出迴圈
loop
rndtest=st
end function
set rs=server.CreateObject("ADODB.RecordSet")
sql="select * from 判斷題 order by id asc"
rs.open sql,conn,1,1
mycound =rndtest(rs.Recordcount, pd) '取得題庫中的試題總數
testcound=split(mycound, "'")
for i=0 to UBound(testcound)
rs.absoluteposition=matrix(i) ‘把記錄指標移指向第testcound (i)條記錄
session("pdda")=session("pdda")&rs("正確答案")&"|" ‘用session來記錄標準答案
‘輸出試題及答案%>
<tr>
<td width="10%" ><%=i%>、<%=rs("題目內容")%></td>
<td align="center" width="10%" ><select name="cate<%=i%>">
<option selected value=True>對</option>
<option value=False>錯</option></select> </td>
</tr>
<%
next
%>
演算法三
由於第二種演算法容易造成試題重複,為了避免系統產生重複的隨機數,我試著將題庫中試題總數均分為kp個範圍,讓每個範圍個產生一個隨機數,這樣就有效地避免了隨機數重複。
<% set rs=server.CreateObject("ADODB.RecordSet")
sql="select * from 判斷題 order by id asc"
rs.open sql,conn,1,1
mycound=rs.Recordcount '取得試題總數
for i=1 to pd
randomize
temp=fix((fix(rs.Recordcount/pd)+1)*rnd+1) ‘產生1~題庫試題總數除以試卷試題數之間的隨機數
rs.move temp ‘指標移到隨機數位置
session("pdda")=session("pdda")&rs("正確答案")&"|" ‘用session來記錄標準答案
‘輸出試題及答案%>
<tr>
<td width="10%" ><%=i%>、<%=rs("題目內容")%></td>
<td align="center" width="10%" ><select name="cate<%=i%>">
<option selected value=True>對</option>
<option value=False>錯</option></select> </td>
</tr>
<%next
rs.close%>
這種演算法能夠有效地解決了演算法一和演算法二的不足,既做到了隨機抽取試題,又做到了試題不重複。但是仔細一想還是存在不足:就是題庫中的每一道試題出現的機率不相同,這樣就顯得不科學了。因為kp次都產生大數位機率是不大了,這樣排在後面的試題出現的機會就很小了。
演算法四
演算法四是我最後的研究結果,其演算法分為三步:
Setp1、擷取試題庫試題總數,然後產生一個1~試題總數的陣列。
Setp2、產生隨機數,將這個矩陣打亂。
Setp3、按順序取出陣列中的題目。
這種演算法和洗牌的原理相類似,圖示如下:
(設試題庫總數為10,要抽取出5道題)
Setp1:
陣列的初始內容如下:
A1 |
A2 |
A3 |
A4 |
A5 |
A6 |
A7 |
A8 |
A9 |
A10 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
Setp2:
產生兩個隨機數,如3和6。然後將A3和A6的內容交換,陣列的內容變為:
A1 |
A2 |
A3 |
A4 |
A5 |
A6 |
A7 |
A8 |
A9 |
A10 |
1 |
2 |
6 |
4 |
5 |
3 |
7 |
8 |
9 |
10 |
Setp3、按順序取出陣列中的題目A1~A5的內容,應該是1、2、6、4、5,讀出資料庫中相應的試題。
如果不斷迴圈Setp2,該陣列中的內容就隨機打亂,這樣既實現了隨機抽取試題的目的,又避免了試題抽取重複。
<%
dim matrix() '定義變數數組
set rs=server.CreateObject("ADODB.RecordSet")
sql="select * from 判斷題order by id asc"
rs.open sql,conn,1,1
mycound=rs.Recordcount '取得試題總數
'設定陣列的初始值
for i=0 to mycound
matrix(i)=i+1
next
randomize '產生隨機數種子
for i=0 to 2*mycound ‘迴圈2*試題總數次
j=fix(rnd*mycound)
k=fix(rnd*mycound)
'交換matrix(k)和matrix(j)的內容
temp=matrix(k)
matrix(k)=matrix(j)
matrix(j)=temp
next
'取出陣列中的題目,數量為試卷中該類題的數量
for i=1 to pd
rs.absoluteposition=matrix(i) ‘把記錄指標移指向第matrix(i)條記錄
session("pdda")=session("pdda")&rs("正確答案")&"|" ‘用session來記錄標準答案
‘輸出試題及答案%>
<tr>
<td width="10%" ><%=i%>、<%=rs("題目內容")%></td>
<td align="center" width="10%" ><select name="cate<%=i%>">
<option selected value=True>對</option>
<option value=False>錯</option></select> </td>
</tr>
<%next
rs.close%>
演算法四補充:
當時無法解決定義一個足夠大的數組,於是用dim matrix(X),預設一個足夠大的數字X。雖然說暫時不會出現問題,但是如果資料庫中的試題數很大呢?總像一塊石頭擱在心中。呵呵。現在發現了ReDim語句,vbs參考中是這樣介紹ReDim 語句的:在過程級中聲明動態陣列變數並分配或重新分配儲存空間。這樣用ReDim matrix (rst.Recordcount)就可以完美地解決了這個問題。
<%
Dim matrix()
set rs=server.CreateObject("ADODB.RecordSet")
sql="select * from 判斷題order by id asc"
rs.open sql,conn,1,1
mycound=rs.Recordcount '取得試題總數
ReDim matrix(rst.Recordcount) ‘定義一個等於rst.Recordcount的數組
'設定陣列的初始值
for i=0 to mycound
matrix(i)=i+1
next
randomize '產生隨機數種子
for i=0 to 2*mycound ‘迴圈2*試題總數次
j=fix(rnd*mycound)
k=fix(rnd*mycound)
'交換matrix(k)和matrix(j)的內容
temp=matrix(k)
matrix(k)=matrix(j)
matrix(j)=temp
next
'取出陣列中的題目,數量為試卷中該類題的數量
for i=1 to pd
rs.absoluteposition=matrix(i) ‘把記錄指標移指向第matrix(i)條記錄
session("pdda")=session("pdda")&rs("正確答案")&"|" ‘用session來記錄標準答案
‘輸出試題及答案%>
<tr>
<td width="10%" ><%=i%>、<%=rs("題目內容")%></td>
<td align="center" width="10%" ><select name="cate<%=i%>">
<option selected value=True>對</option>
<option value=False>錯</option></select> </td>
</tr>
<%next
rs.close%>
總結:
相對來說,演算法四應該是最合理的。但是我不知道應該迴圈Setp2多少次,陣列中的數值最“隨機”,而且我不知道當試題庫的試題總數很多的情況下,這種演算法是否會很占系統資源,歡迎大家來信和我討論。