寫這篇博客的起因是在牛客上刷到了一道約瑟夫環相關的題。牛客鏈接
在牛客上跑通過了,本着追求機器效率的原則,去leetcode上找到了同樣的題,再跑了一遍,發現超時。看了幾篇博客並思索許久后打算寫這篇博客來探究
約瑟夫環問題在選取不同數據結構和不同處理方法的時候時間復雜度的優劣。leetcode鏈接
關於約瑟夫環及其遞推公式請見:https://blog.csdn.net/u011500062/article/details/72855826
假設 每數 m 個數就去掉一個人,比如 m = 3,從第一個人開始,則數1,2,3 到第三個人的時候,第三個人消失。
公式:當前輪位置為P(P=1,2,3)的人,下一輪的位置 next = (P - m) % 當前輪人數 D
有公式: next + k * D = P - m,所以有 P = m + next + k * D,所以有 P = (m + next + k * D)
P % D = ( m + next ) % D,因為 m + next 可能會超出 D,所以需要 % D。以此落到 D 上。其實就是下面的位置+m 然后對上一輪人數取余,就可以得到上一輪的位置
需要解釋的是為什么受害者之前位置的人在下一輪的下標是(當前下標 - m)% 當前一輪的人數
需要注意的是,當位置P = 0,的時候,代表的是上一輪的最后一個元素。
m = 3
(1 + x) % D 很容易把 x 寫成人數 D
1 2 3 4 5【(1 + 3)% 5 = 4】
4 5 1 2 【(2 + 3)% 4 = 1】
2 4 5【(2 + 3)% 3 = 2】
2 4 【(1 + 3)% 2 = 0】 這里需要把 0 換成 2,因為當前有兩個數,0代表最后一個數
4
這樣算到第
分析時間復雜度:
如果要去直接模擬約瑟夫問題的各個環節的話,因為每次都要報數報m次,不考慮具體是什么數據結構實現存儲,n個人需要報 n - 1次數,那么問題規模是O(n * m)
考慮具體使用的數據結構,比如數組,0號位代表一號人,1號位代表二號人,以此類推,那么有兩種方法:
1.每當有一個人遇害,就把這個人從數組中刪去,並且將此人后面的人向前移動。
2.只是把遇害者的位置標記,不刪除該位置,之后訪問的時候檢查到某個位置的值之前標記過的話就跳過(沒有人在這個位置上)
第一種情況,總共要挪動多少人 具體是難以推測的,因為每次都是以m為位移在當前數組上找下一個位置,但是可以假設最壞情況,每次移動的人數是當前數組人數 - 1
那么總的移動次數是 : (n - 1) + (n - 2) + (n - 3) + ...... + 1 = (n - 1) * n / 2 , 是 O(n ^ 2)級的。
第二種情況,屬於暴力遍歷,如果路途中沒有遇到之前的遇害者位置,那么訪問次數是 m,但是每有一個人遇害,遇害者位置就會 + 1, 所以每次的遍歷次數 > m,越遍歷下去情況越糟糕,因為人死得越來越多,需要訪問-跳過的槽位也越來越多。總次數 > n * m
用Java實現:
其實第一種情況,對應於使用ArrayList,因為他的底層實現就是數組,並且每次刪除元素都會把后面的元素平移。
第二種情況,沒有什么類似的ADT實現,但是LinkedList有類似之處,LinkedList不能像ArrayList那樣直接通過下標找到元素,而是需要一個節點一個節點地next來獲取某個
下標位置的元素
至於公式推導法......因為是從 2 到 n 遍歷,相當於線性時間復雜度 O (n),真的是數學可以造成降維打擊......