仅使用这些黑盒函数对数组进行排序的最快方法?

时间:2019-07-17 14:53:22

标签: arrays algorithm sorting

问题如下:

  

作为输入,您将获得一个大小为 N 的数组( ARR ),您不知道–的内容,也无法看到–和预先指定的值 U≪N 。您的解决方案应适用于 U N 的任何值。

     

您将得到一个大小为 N 的工作数组( W )。您还可以使用以下同时在 O(1)时间运行的帮助器函数:

     
      
  • SORT(A,B):SORT 将采用数组 ARR W 的两个连续部分,大小为< em> | A |≤U和 | B |≤U,将它们合并为一个新数组,对它们进行排序,然后将大小为 | A | 返回到 A ,其余值返回到 B 。它将返回 A 中剩余的 A 中值的数量。

  •   
  • COPY(A-> C):COPY 会将 A 的内容复制到 C 中。 | A | == | C |

  •   

我已经有解决此问题的方法,它是合并排序的变体,并且具有类似的运行时间 O((N / U)log(N / U)),但我当时想知道是否有更快的解决方案。

此外,我想知道如何在没有工作数组( W )的约束下如何对数组进行排序。


根据要求,我的解决方案(非常非正式和简化)

# Define helper subroutine MERGE (merges two sorted arrays)
MERGE (array A[1...m], array B[1...n]):
  a_idx = 1
  b_idx = 1
  w_idx = 1
  while a_idx < m+1 and b_idx < n+1; do
    COPY( A[a_idx ... a_idx+U], W[w_idx ... w_idx+U])
    COPY( B[b_idx ... b_idx+U], W[w_idx+U+1 ... w_idx+2U])
    F=W[w_idx ... w_idx+U]
    G=W[w_idx+U+1 ... w_idx+2U]
    offset = SORT(F,G)
    a_idx += offset
    b_idx += (U-offset)
    w_idx += U
  end while

  if a_idx != m+1:
    # this can be done in a while loop as well but just writing it down like this for simplification
    COPY( A[a_idx ... m+1], W[w_idx ... w_idx+(m+1-a_idx)])
    w_idx += (m+1-a_idx)
  end if

  if b_idx != n+1:
    # this can be done in a while loop as well but just writing it down like this for simplification
    COPY( B[b_idx ... n+1], W[w_idx ... w_idx+(n+1-b_idx)])
    w_idx += (n+1-b_idx)
  end if

  COPY(W[1 ... m], A[1 ... m])
  COPY(W[m+1 ... m+n], B[1 ... n])
end MERGE

有了此子例程,您可以使用 SORT 轻松地将原始数组排序为大小为 2U 的已排序片段,然后将合并排序逻辑与此例程结合使用分成一个完整的数组。

1 个答案:

答案 0 :(得分:2)

您的算法看起来是个好主意,尽管我无法通过实际实现使它起作用。

时间复杂度

不能期望比 O(N / U log(N / U))做的更好,因为SORT函数是唯一可用于获取有关比较信息和执行置换的方法,并且最多可以作用于2U数据元素。就像SWAP功能一样:SWAP订购2个值,SORT订购2U值。

想象一个特殊情况的输入:

  • 它的大小是U的倍数。
  • 从U的倍数(从零开始)的索引开始,每个大小为U的单独块(子数组)都已被排序
  • 一个块的最小-最大范围不与另一个块的最小-最大范围重叠。因此,例如,您将不会有U = 3的[1,3,5,2,4,6],因为1-5与2-6重叠。另一方面,U = 3的[7,9,11,1,5,6]符合此条件

因此,有了这样的输入,排序的问题就减少到了对N / U块的排序。这些块保持不变。一对块上的SORT就像SWAP。这就像对M = N / U个元素的常规排序,即 O(MlogM)。我不知道将SORT应用于跨块范围如何以某种方式加快排序速度,因为它会破坏所涉及的块中已经实现的顺序。因此,我认为您能得到的最好的是 O(N / U log(N / U))

不使用W或COPY的解决方案

人们可以使用堆排序的思想,仅通过SORT函数对输入进行排序。

选择堆排序的依据是观察到,heapify和siftDown函数(通常是堆排序)只能通过交换实现,而无需暂时保留其中一个值。

我会考虑在固定索引边界上使用“块”,这意味着每个块都占据[iU, (i+1)U]范围(假定索引从零开始,并且最终索引值为而不是中包含)。最后一个块可能会更小,因为输入大小可能并不总是U的倍数。每个块都将被视为堆中的一个节点。

堆排序仍需要进行一些调整以使其起作用:

  • Max Heap中的heap属性表示,父值不应小于其任何子值。在处理块时,我们的目标是确保父节点中的 all 值不少于每个子节点中的 all 值。换句话说,父块的最小值不应小于每个子块的最大值。

  • 在siftDown过程中,堆排序将比较要筛选的节点的两个子代的值,然后选择值最大的子代。在我们的案例中,我们没有纯粹的比较功能,但是我们可以使用SORT对两个子项进行排序,以使它们的值范围不再重叠。但是,这可能会破坏孙子项的heap属性。为避免这种情况,我们必须确保在两个子项中具有最小值的子项 keeps 在排序后保持该最小值。换句话说,我们应该将该孩子作为对SORT的调用的第一个参数。显然,这保证了该堆属性与该孩子的孩子在一起。但是同胞也不能获得比调用SORT之前的最小值更小的值。这样,该子项也将使用其自己的子项维护heap属性。

  • 上一点仍未解决,如何确定在两个子项中具有最小价值的子项(需要决定如何对SORT的参数进行排序)。为此,我们首先要确保每个子项都已被单独排序,以使每个子项在块的第一个插槽中具有最小值。然后,我们可以仅使用左孩子和右孩子的最左边的值(长度为1的数组)来调用SORT。如果这导致交换,则SORT的返回值将为0。在这种情况下,我们将使用取反的参数再次调用SORT,以撤消交换。但是第一个返回值会告诉我们哪个是最小值。

  • 在heapify函数中,我们将确保所有叶节点都已排序。标准堆堆方法的其余部分也将自动对每个内部节点进行排序。

  • 实际的siftDown步骤会将父节点的值与最大子节点的值进行比较,如果子节点的值大于父节点的值,则执行交换。在我们的例子中,所有这些都可以通过调用SORT来完成(通过上述方法选择了子对象之后):将该子块作为第一个参数传递,将父块作为第二个参数传递。这将保证父级和子级之间以及与另一个子级(如果有)之间的堆属性,因为我们已经确定该另一个子级中的所有值均不大于所选子级中的所有值。

  • 在边界情况下,最后一片叶子的块大小较小。当它也有一个同级对象时,这可能变得很难处理,并且siftDown操作可能会选择最右边的较短的块。为避免该叶子成为siftDown选择的候选者,我们将确保其叶子成为“更大”的孩子。始终可以通过使用SORT对两个叶子进行排序(将较短的块作为第一个参数)来完成,因为我们不必担心维护带有孙子元素的heap属性(没有)。当较短的叶子是其父对象的唯一子代时,就没有问题。

实施

我在这里提供了JavaScript的实现,而不是伪代码,我认为它非常易读。

下面的代码段允许输入数组值和U值。结果将实时更新。

// Implementation of the SORT function (not part of the solution)
function createSortFunction(maxChunkSize) {
    // Given the value U, a specific function is created that will throw 
    // when U is exceeded
    return function sort(arr, aStart, aEnd, bStart, bEnd) {
        if (aEnd - aStart > maxChunkSize || bStart - bEnd > maxChunkSize) {
            throw new Error("SORT called with a too large range");
        }
        const aLength = aEnd - aStart;
        let sorted = [...arr.slice(aStart, aEnd), ...arr.slice(bStart, bEnd)]
                .map((x, i) => [x, i]) // temporarily store the unsorted index with each value
                .sort(([x],[y]) => x-y); //... then sort numerically
        // Count the number of values in the first chunk that originally came from A (had a low index)   
        let count = sorted.slice(0, aEnd-aStart).reduce((count, [,i]) => count + (i < aLength), 0);
        sorted = sorted.map(([x]) => x); // remove the temporary index info
        // Populate the original arrays with the sorted values
        arr.splice(aStart, aLength, ...sorted.splice(0, aLength));
        arr.splice(bStart, bEnd-bStart, ...sorted);
        return count;
    }
}

function blackboxSort(arr, maxChunkSize, sort) {
    // Sorting algorithm based on HeapSort, but only using
    //    the given sort function for any data access
    let numCalls = 0;
    let numChunks = Math.ceil(arr.length / maxChunkSize);

    // First some local functions:
    function chunkRange(i, size) {
        // Given a chunk-number, return the corresponding start-end indexes in the array
        // Last chunk may be smaller in size when array size is not multiple of chunk size
        i *= maxChunkSize;
        return [Math.min(i, arr.length), Math.min(i + size, arr.length)];
    }
    
    // Wrapper around SORT function:
    function chunkPairSort(i, j, size=maxChunkSize) {
        let [iStart, iEnd] = chunkRange(i, size);
        let [jStart, jEnd] = chunkRange(j, size);
        numCalls++; // Keep track of how many calls to SORT are made
        // Return true when the call to SORT exchanged at least one value between the chunks
        return sort(arr, iStart, iEnd, jStart, jEnd) < iEnd - iStart;
    }
    
    function isLessThan(i, j) {
        // Returns true when the first value in the first chunk is smaller 
        // than the first value in the second chunk. The second call will
        // undo the change if the first call made a swap:
        return !chunkPairSort(i, j, 1) || !chunkPairSort(j, i, 1);
    }
    
    function siftDown(parent, numChunks) {
        let child = parent*2+1;
        let dirty = true;
        while (dirty && child < numChunks) {
            if (child + 1 < numChunks) { // There are 2 children
                // We know that each child has its values already sorted.
                // Check which of the two children has the smallest value.
                // Then sort the two children in a way that keeps the smallest value 
                // in the child where it currently is. However, if the children are
                // leaves, always sort with the leftmost child getting the greatest values,
                // so we are sure the child with the greatest values has a full chunk size.
                if (child*2+1 < numChunks && isLessThan(child, child+1)) {
                    chunkPairSort(child, child+1);
                    child++; // pick the greatest child.
                } else {
                    chunkPairSort(child+1, child);
                }
            }
            // Perform the actual sift-down-swap
            dirty = chunkPairSort(child, parent);
            // Walk further down the heap
            parent = child;
            child = child*2+1;
        }
    }
    
    function heapify(numChunks) {
        const lastParent = (numChunks-2) >> 1;
        // We would not do this loop in standard heap sort.
        // However, we need each leaf in the future-heap to have the smallest value in first position,
        //   as the implementation of siftDown assumes this.
        for (let chunk = numChunks - 1; chunk > lastParent; chunk-=2) {
            chunkPairSort(chunk-1, chunk);
        }
        // Standard loop in Floyd's algorithm:
        for (let parent = lastParent; parent >= 0; parent--) {
            siftDown(parent, numChunks);
        }
    }
    
    // Main:
    
    if (arr.length <= 2*maxChunkSize) { // Trivial case:
        chunkPairSort(0, 1);
    } else {
        // Build heap: Floyd's method = O(n) where n = number of chunks
        heapify(numChunks);
        // Now all chunks have their values sorted, and are in a maxheap order
        // Extract chunks from the maxheap and place them just after the reduced heap = O(nlogn)
        for (let size = numChunks-1; size > 0; size--) {
            // Swap first chunk with last chunk
            // The first time this may not be a pure swap: when the last chunk is smaller in size.
            //   But even then the effect is fine: the largest values will end up
            //   in that smaller chunk, and the smaller in the (larger) root.
            chunkPairSort(0, size);
            // Root is now "dirty", so sift it down the (reduced) heap
            siftDown(0, size);
        }
    }
    return numCalls;
}

// Snippet I/O handling
function refresh () {
    let array = (document.querySelector("#array").value.match(/-?\d+/g) || []).map(Number);
    let maxChunkSize = Math.max(1, document.querySelector("#U").value) || 1;
    let numCalls = blackboxSort(array, maxChunkSize, createSortFunction(maxChunkSize));
    document.querySelector("#sorted").textContent = JSON.stringify(array);
    document.querySelector("#calls").textContent = numCalls;
}
(document.oninput = refresh)();
document.querySelector("#random").onclick = function () {
    document.querySelector("#array").value = Array.from({length: 50}, () => ~~(Math.random() * 1000)).join` `;
    refresh();
}
Input array: <button id="random">Random</button><br>
<input id="array" style="width:100%" value="5 3 8 4 0 2 7 9 11 1 10 8"><br>
U: <input id="U" type="number" value="3" style="width: 3em"><br>
Result: <span id="sorted"></span><br>
Calls made to sort: <span id="calls"></span>