“不排序”快速排序

3
(注意!虽然我知道Python有足够的排序选项,但这段代码更像是一个通用的概念验证,并且将在以后移植到另一种语言,因此我不能使用任何特定的Python库或函数。 此外,您提供的解决方案不一定必须遵循我下面的方法。)

背景

我有一个快速排序算法,并尝试实现一种方法来允许稍后对已排序元素的新位置进行“取消排序”。也就是说,如果元素A在索引x处并且被排序到索引y,则“指针”(或者根据您的术语,引用或映射)数组会将其索引x处的值从x更改为y

更详细地说:
您首先使用一个数组arr开始程序,其中包含一些给定的数字集。稍后,将该数组通过快速排序算法运行,因为对其进行排序对于之后对其进行处理很重要。

该数组的排序顺序很重要。因此,您还有另一个数组ref,其中包含原始数组的索引,以便当您将参考数组映射到数组时,可以重现原始数组的排序顺序。

在对数组进行排序之前,数组和映射如下所示:

arr = [1.2, 1.5, 1.5, 1.0, 1.1, 1.8]
ref = [0,   1,   2,   3,   4,   5]
--------
map(arr,ref) -> [1.2, 1.5, 1.5, 1.0, 1.1, 1.8]

你可以看到ref的索引0指向arr的索引0,给你1.2ref的索引1指向arr的索引1,给你1.5,以此类推。

当算法排序时,应重新排列ref,使得按照上述过程进行映射时,生成预排序的arr

arr = [1.0, 1.1, 1.2, 1.5, 1.5, 1.8]
ref = [2,   3,   4,   0,   1,   5]
--------
map(arr,ref) -> [1.2, 1.5, 1.5, 1.0, 1.1, 1.8]

对于ref数组,索引0的值为2,因此映射数组的第一个元素是arr[2]=1.2。索引1的值为3,因此映射数组的第二个元素是arr[3]=1.5,以此类推。

问题

目前我的代码实现在排序方面表现非常出色,但在重新映射ref时表现非常糟糕。

给定相同的arr数组,程序的输出如下:

arr = [1.0, 1.1, 1.2, 1.5, 1.5, 1.8]
ref = [3,   4,   0,   1,   2,   5]
--------
map(arr,ref) -> [1.5, 1.5, 1.0, 1.1, 1.2, 1.8]

这是一个问题,因为该映射表明的内容绝对等同于原始内容:
[1.5, 1.5, 1.0, 1.1, 1.2, 1.8] != [1.2, 1.5, 1.5, 1.0, 1.1, 1.8]

我的做法是这样的:
  1. 当在数组arr中索引为x和y处的元素a和b被交换时,
  2. 那么设置ref[x] = y和ref[y] = x。
这种方法目前不起作用,我无法想到另一种不需要O(n^2)时间复杂度的解决方案。
谢谢!

最小可重现示例

testing = [1.5, 1.2, 1.0, 1.0, 1.2, 1.2, 1.5, 1.3, 2.0, 0.7, 0.2, 1.4, 1.2, 1.8, 2.0, 2.1]

# This is the 'map(arr,ref) ->' function
def print_links(a,b):
    tt = [a[b[i]-1] for i in range(0,len(a))]
    print("map(arr,ref) -> {}".format(tt))

    # This tests the re-mapping against an original copy of the array
    f = 0
    for i in range(0,len(testing)):
        if testing[i] == tt[i]:
            f += 1

    print("{}/{}".format(f,len(a)))

def quick_sort(arr,ref,first=None,last=None):
    if first == None:
        first = 0
    if last == None:
        last = len(arr)-1

    if first < last:
        split = partition(arr,ref,first,last)
        quick_sort(arr,ref,first,split-1)
        quick_sort(arr,ref,split+1,last)

def partition(arr,ref,first,last):
    pivot = arr[first]

    left = first+1
    right = last

    done = False
    while not done:
        while left <= right and arr[left] <= pivot:
            left += 1

        while arr[right] >= pivot and right >= left:
            right -= 1

        if right < left:
            done = True
        else:
            temp = arr[left]
            arr[left] = arr[right]
            arr[right] = temp

            # This is my attempt at preserving indices part 1
            temp = ref[left]
            ref[left] = ref[right]
            ref[right] = temp

    temp = arr[first]
    arr[first] = arr[right]
    arr[right] = temp

    # This is my attempt at preserving indices part 2
    temp = ref[first]
    ref[first] = ref[right]
    ref[right] = temp

    return right

# Main body of code
a = [1.5,1.2,1.0,1.0,1.2,1.2,1.5,1.3,2.0,0.7,0.2,1.4,1.2,1.8,2.0,2.1]
b = range(1,len(a)+1)

print("The following should match:")
print("a = {}".format(a))
a0 = a[:]
print("ref = {}".format(b))
print("----")
print_links(a,b)

print("\nQuicksort:")
quick_sort(a,b)
print(a)

print("\nThe following should match:")
print("arr = {}".format(a0))
print("ref = {}".format(b))
print("----")
print_links(a,b)

你使用的是哪个版本的Python?如果这是Python 3(如你的“print”所示),当你尝试对传入的range对象进行赋值时,代码会失败。 - Prune
1
我正在运行 Python 2.7.10。 - Daniel R. Livingston
5个回答

3
您可以完成您的要求,但是在实际操作中,我们通常会修改排序比较函数而不是交换函数。通常,常见编程语言提供的排序程序都内置了这种功能,因此您不必编写自己的排序程序。
在此过程中,您通过指向arr值的值对ref数组(下面称为order)进行排序。这将生成与您已经拥有的相同的ref数组,但是不会修改arr。
使用此排序方式对原始数组进行排序。您期望它使已排序的数组失序,这就是您的代码无法正常工作的原因。
您可以反转此排序顺序以获取最初寻找的ref数组,或者当需要排序时,您可以将其保留未排序并通过order映射。
arr = [1.5, 1.2, 1.0, 1.0, 1.2, 1.2, 1.5, 1.3, 2.0, 0.7, 0.2, 1.4, 1.2, 1.8, 2.0, 2.1]

order = range(len(arr))
order.sort(key=lambda i:arr[i])

new_arr = [arr[order[i]] for i in range(len(arr))]

print("original array = {}".format(arr))
print("sorted ordering = {}".format(order))
print("sorted array = {}".format(new_arr))

ref = [0]*len(order)
for i in range(len(order)):
    ref[order[i]]=i

unsorted = [new_arr[ref[i]] for i in range(len(ref))]
print("unsorted after sorting = {}".format(unsorted))

输出:

original array = [1.5, 1.2, 1.0, 1.0, 1.2, 1.2, 1.5, 1.3, 2.0, 0.7, 0.2, 1.4, 1.2, 1.8, 2.0, 2.1]
sorted ordering = [10, 9, 2, 3, 1, 4, 5, 12, 7, 11, 0, 6, 13, 8, 14, 15]
sorted array = [0.2, 0.7, 1.0, 1.0, 1.2, 1.2, 1.2, 1.2, 1.3, 1.4, 1.5, 1.5, 1.8, 2.0, 2.0, 2.1]
unsorted after sorting = [1.5, 1.2, 1.0, 1.0, 1.2, 1.2, 1.5, 1.3, 2.0, 0.7, 0.2, 1.4, 1.2, 1.8, 2.0, 2.1]

很棒的回答,Matt,谢谢你。不幸的是,我将使用的语言 - Fortran - 没有内置排序或比较函数。 - Daniel R. Livingston
1
你可以编写自己的排序算法。改变比较而非交换仍然更有效率。 - Matt Timmermans

1
我认为你可以在完成后修复你的ref数组。从你的代码示例中,只需在调用 quick_sort(a,b) 后插入以下片段即可。
c = range(1, len(b)+1)
for i in range(0, len(b)):
    c[ b[i]-1 ] = i+1

c 数组现在应该包含正确的引用。


嗯,不太确定。我没有仔细阅读你在快速排序中如何生成“ref”数组,所以我不知道它是如何被错误计算的。相反,我只看了一下你正确和不正确的“ref”数组的示例,并看到了它们的模式。具体来说,在正确的“ref”数组中,0位于位置3,1位于位置4,2位于位置0等等...这恰好是不正确的“ref”数组中的条目。 - mhum

1

借鉴@Prune的话:在b中,你拥有的是前向转换,即排序本身。将其应用到a0上可提供已排序的列表(print_links(a0,b))。
您只需要通过查找哪个元素去了哪个位置来撤销它:

c=[b.index(i)+1 for i in range(1,len(a)+1)]
print_links(a,c)

1
你不需要维护索引和元素的映射关系,只需像对待数组一样对索引进行排序。例如:
unsortedArray =  [1.2, 1.5, 2.1]
unsortedIndexes = [0,   1,   2]
sortedAray = [1.2, 1.5, 2.1]

然后,您只需在排序unsortedArray时交换0和1,并获取排序后的索引[1, 0, 2],您可以通过sortedArray[1],sortedArray[0],sortedArray[2]获得原始数组。
def inplace_quick_sort(s, indexes, start, end):
    if start>= end:
        return
    pivot = getPivot(s, start, end)#it's should be a func
    left = start
    right = end - 1
    while left <= right:
        while left <= right and customCmp(pivot, s[left]):
        # s[left] < pivot:
            left += 1
        while left <= right and customCmp(s[right], pivot):
        # pivot < s[right]:
            right -= 1
        if left <= right:
            s[left], s[right] = s[right], s[left]
            indexes[left], indexes[right] = indexes[right], indexes[left]
            left, right = left + 1, right -1
    s[left], s[end] = s[end], s[left]
    indexes[left], indexes[end] = indexes[end], indexes[left]
    inplace_quick_sort(s, indexes, start, left-1)
    inplace_quick_sort(s, indexes, left+1, end)
def customCmp(a, b):
        return a > b
def getPivot(s, start, end):
    return s[end]
if __name__ == '__main__':
    arr = [1.5,1.2,1.0,1.0,1.2,1.2,1.5,1.3,2.0,0.7,0.2,1.4,1.2,1.8,2.0,2.1]
    indexes = [i for i in range(len(arr))]
    inplace_quick_sort(arr,indexes, 0, len(arr)-1)
    print("sorted = {}".format(arr))
    ref = [0]*len(indexes)
    for i in range(len(indexes)):
        #the core point of Matt Timmermans' answer about how to construct the ref
        #the value of indexes[i] is index of the orignal array
        #and i is the index of the sorted array,
        #so we get the map by ref[indexes[i]] = i
        ref[indexes[i]] = i
    unsorted = [arr[ref[i]] for i in range(len(ref))]
    print("unsorted after sorting = {}".format(unsorted))

1

这并不可怕:你只是反转了引用的使用。你的索引ref告诉你如何从原始列表构建排序列表。但是,你把它应用于相反的方向:尝试重构原始列表的排好序的列表。你需要反向映射。

这足以让你解决问题吗?


谢谢你的回答,Prune。我一直在尝试解构它,但我不太确定如何获得这个arrref关系的反向映射。 - Daniel R. Livingston
1
看看你交换索引的逻辑。目前,你是根据“我把原来在索引k处的元素移动到哪里了(对于每个范围中的k)?”这个想法来做的。相反,你需要从“对于当前在索引k处的元素,它最初在哪里?”这个角度来思考。 - Prune
1
提示:您的交换引用需要从当前版本开始向后工作。尝试在纸上使用4或5个元素的数组进行计算。 - Prune
你能再解释一下吗?我试着在纸上计算了一下,似乎只需要在ref中与arr交换相同两个索引处的元素就可以得到期望的结果。 - Daniel R. Livingston

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接