Algorithm for Shuffling a Linked List in n log n time

后端 未结 7 517
北荒
北荒 2021-01-30 05:37

I\'m trying to shuffle a linked list using a divide-and-conquer algorithm that randomly shuffles a linked list in linearithmic (n log n) time and logarithmic (log n) extra space

7条回答
  •  夕颜
    夕颜 (楼主)
    2021-01-30 06:05

    Code

    Up shuffle approach

    This (lua) version is improved from foxcub's answer to remove the need of dummy nodes.

    In order to slightly simplify the code in this answer, this version suppose that your lists know their sizes. In the event they don't, you can always find it in O(n) time, but even better: a few simple adaptation in the code can be done to not require to compute it beforehand (like subdividing one over two instead of first and second half).

    function listUpShuffle (l)
        local lsz = #l
        if lsz <= 1 then return l end
    
        local lsz2 = math.floor(lsz/2)
        local l1, l2 = {}, {}
        for k = 1, lsz2     do l1[#l1+1] = l[k] end
        for k = lsz2+1, lsz do l2[#l2+1] = l[k] end
    
        l1 = listUpShuffle(l1)
        l2 = listUpShuffle(l2)
    
        local res = {}
        local i, j = 1, 1
        while i <= #l1 or j <= #l2 do
            local rem1, rem2 = #l1-i+1, #l2-j+1
            if math.random() < rem1/(rem1+rem2) then
                res[#res+1] = l1[i]
                i = i+1
            else
                res[#res+1] = l2[j]
                j = j+1
            end
        end
        return res
    end
    

    To avoid using dummy nodes, you have to compensate for the fact that the two intermediate lists can have different lengths by varying the probability to choose in each list. This is done by testing a [0,1] uniform random number against the ratio of nodes popped from the first list over the total number of node popped (in the two lists).

    Down shuffle approach

    You can also shuffle while you subdivide recursively, which in my humble tests showed slightly (but consistently) better performance. It might come from the fewer instructions, or on the other hand it might have appeared due to cache warmup in luajit, so you will have to profile for your use cases.

    function listDownShuffle (l)
        local lsz = #l
        if lsz <= 1 then return l end
    
        local lsz2 = math.floor(lsz/2)
        local l1, l2 = {}, {}
        for i = 1, lsz do
            local rem1, rem2 = lsz2-#l1, lsz-lsz2-#l2
            if math.random() < rem1/(rem1+rem2) then
                l1[#l1+1] = l[i]
            else
                l2[#l2+1] = l[i]
            end
        end
    
        l1 = listDownShuffle(l1)
        l2 = listDownShuffle(l2)
    
        local res = {}
        for i = 1, #l1 do res[#res+1] = l1[i] end
        for i = 1, #l2 do res[#res+1] = l2[i] end
        return res
    end
    

    Tests

    The full source is in my listShuffle.lua Gist.

    It contains code that, when executed, prints a matrix representing, for each element of the input list, the number of times it appears at each position of the output list, after a specified number of run. A fairly uniform matrix 'show' the uniformity of the distribution of characters, hence the uniformity of the shuffle.

    Here is an example run with 1000000 iteration using a (non power of two) 3 element list :

    >> luajit listShuffle.lua 1000000 3
    Up shuffle bias matrix:
    333331 332782 333887
    333377 333655 332968
    333292 333563 333145
    Down shuffle bias matrix:
    333120 333521 333359
    333435 333088 333477
    333445 333391 333164
    

提交回复
热议问题