带你了解煎饼排序:求煎饼排序的翻转过程

858 阅读1分钟

正题

969. 煎饼排序

给你一个整数数组 arr ,请使用 煎饼翻转 **完成对数组的排序。

一次煎饼翻转的执行过程如下:

  • 选择一个整数 k ,1 <= k <= arr.length
  • 反转子数组 arr[0...k-1]下标从 0 开始

例如,arr = [3,2,1,4] ,选择 k = 3 进行一次煎饼翻转,反转子数组 [3,2,1] ,得到 arr = [1,2,3,4] 。

以数组形式返回能使 arr 有序的煎饼翻转操作所对应的 k 值序列。任何将数组排序且翻转次数在 10 * arr.length 范围内的有效答案都将被判断为正确。

解析:

什么是煎饼排序

首先了解一下煎饼排序的定义:饼排序是数量问题的口语术语,当刮刀可以插入堆叠中的任何点并用于翻转其上方的所有薄饼时,按照尺寸的顺序对无序堆叠的薄煎饼进行分类。 煎饼数是给定数量的煎饼所需的最小翻转数。 在这种形式下,问题首先由美国几何学家Jacob E. Goodman讨论过。它是排序问题的变体,其中唯一允许的操作是反转序列的某些前缀的元素。 与传统的排序算法不同,传统的排序算法试图以尽可能少的比较进行排序,目标是尽可能少地对序列进行排序。 该问题的一个变体涉及烧焦的煎饼,其中每个煎饼具有烧焦的一面,并且所有的煎饼必须另外在底部烧焦的一面。 -- 来自百度

说实话我们不必去考虑它如何定义的,只要去了解它的原理是什么样的,如何去实现的?

一张图带你了解:

1.gif

从图中可以看出,所谓“翻煎饼”就是将 指定范围内的元素倒序。经过多次反复的翻转之后,可以达到排序的目的了。

煎饼排序的原则是什么

了解煎饼排序的原则也是我们实现煎饼排序的基本。可以发现我们翻一次煎饼,就相当于将某一个元素放到了最上面,再翻一次的时候就可以翻到指定的位置。如上图,我想要把9翻到倒数第二的位置,那么我先在9的位置做一次翻转,9就到了最上面,然后再在倒数第二的位置做一次翻转,这样9就到了倒数第二的位置了。所以,我们可以发现,任何元素通过2次翻转都可以到达你想要的位置,这也是煎饼排序的底层逻辑。

实现煎饼排序

根据上面的原则,我们首先需要找到要排序的元素,再确定他要排序的位置,这样对这个元素,做两次翻转就可以了。

找到要排序的元素

第一个要排序的元素是最大的元素,第二个要排序的元素是第二大的元素。。。以此类推。通过代码,我们可以理解为我们要找第 n 大的元素, n 为已经排好序的元素个数 + 1.例如,我们要找第三大元素,那么也可以理解为已经排好了2个元素(2+1)。

设已经排好的元素个数为 let ready = 0,那么我们只要在两次翻转过后执行 ready++即可

实现找到第 ready + 1 大的元素:

let getMaxIndex = function (arr, ready) {
    let maxIndex = 0
    for (let index = 1 ; index < arr.length - ready; index++) {
        maxIndex = arr[maxIndex] >  arr[index] ? maxIndex : index
    }
    return maxIndex
}

实现两次翻转

通过双指针的方法实现数组的翻转,具体原理可以参考之前的文章:反转数组

翻转的方法:

let trans = function (arr, index) {
    let left = 0
    let right = index
    while(left < right) {
        arr[left] = arr[left] ^ arr[right]
        arr[right] = arr[left] ^ arr[right]
        arr[left] = arr[left] ^ arr[right]
        left++
        right--
    }
    return arr
}

实现两次翻转:

 const maxIndex = getMaxIndex(arr, ready)
arr = trans(arr,maxIndex)
arr = trans(arr, arr.length - 1 - ready)

最终代码:

/**
 * @param {number[]} arr
 * @return {number[]}
 */

let getMaxIndex = function (arr, ready) {
    let maxIndex = 0
    for (let index = 1 ; index < arr.length - ready; index++) {
        maxIndex = arr[maxIndex] >  arr[index] ? maxIndex : index
    }
    return maxIndex
}

let trans = function (arr, index) {
    let left = 0
    let right = index
    while(left < right) {
        arr[left] = arr[left] ^ arr[right]
        arr[right] = arr[left] ^ arr[right]
        arr[left] = arr[left] ^ arr[right]
        left++
        right--
    }
    return arr
}

var pancakeSort = function(arr) {
    let ready = 0
    let res = []
    while(ready < arr.length - 1) {
        const maxIndex = getMaxIndex(arr, ready)
        arr = trans(arr,maxIndex)
        arr = trans(arr, arr.length - 1 - ready)
        maxIndex > 0 ? res.push(maxIndex + 1) : ''
        arr.length - 1 - ready > 0 ? res.push(arr.length - 1 - ready + 1) : ''
        ready++
    }
    return res
};

期间有一点可以优化的地方:

maxIndex > 0 ? res.push(maxIndex + 1) : ''

arr.length - 1 - ready > 0 ? res.push(arr.length - 1 - ready + 1) : ''

即当需要翻转的 index = 0 的时候就不需要做翻转了。