【问题标题】:Is this implementation of bubble sort wrong?这种冒泡排序的实现是错误的吗?
【发布时间】:2019-12-14 14:25:38
【问题描述】:

不使用通常的 do-while 循环进行排序,如果我们使用 2 个 for 循环,可以吗?

let bubbleSort = (arr) => {
    console.log(arr);
    for(let i=0; i<arr.length; i++) {
        for (j=0; j<arr.length; j++) {
            if (arr[j] > arr[j+1]){
                console.log("swapped (i,j)->", arr[j],arr[j+1])   
                let temp = arr[j+1];
                arr[j+1] = arr[j];
                arr[j] = temp;

            }
        }
    }

    return arr

}

这会返回正确的排序数组,但它是否比 do-while 循环更好。两者都是 O(n^2) 时间复杂度。

【问题讨论】:

  • “arr[arr.length-1]”与“arr[arr.length]”比较时不会报错吗?
  • 这个版本缺少冒泡排序的一个重要特性——在上次扫描没有交换时停止
  • 您要求我们为您做作业吗?进行冒泡排序的唯一原因是学术理解,在生产应用程序中使用冒泡排序是违反直觉的。
  • @AdrianBrand 并非所有关于 SO 的代码问题都需要与生产代码有关。我们欢迎纯粹的学术和教育问题。作业是on-topic,但在这种情况下做出这样的假设似乎有些冒昧。
  • 不,这不是为了学术目的,我是在自学冒泡排序并阅读了理论并尝试实施它,并希望对我做错的地方提供反馈。

标签: javascript sorting bubble-sort


【解决方案1】:

所有循环类型dowhilefor 都是等效的。它们只是程序员的语法糖,最终在运行时归结为相同的东西。

您在此处发布的是冒泡排序的基本正确实现,但也有一些改进点:

  • 错误:代码正在访问内部循环中的越界索引:由于 arr[j+1] 语句,迭代到 j &lt; arr.length - 1 而不是 j &lt; arr.length。大多数语言会因越界数组访问而崩溃或表现出未定义的行为,但 JS 只是将值与 undefined 进行比较并继续。

  • 潜在错误:内部循环在内部循环中创建了一个全局 j 变量(感谢 Kaiido 指出这一点)。此变量的值将在此函数之外持续存在,并可能导致程序其他地方出现意外行为。使用let(或const,如果不应该重新分配变量)声明变量,以确保它们的范围是本地块。

  • 在外循环的第一次迭代之后,数组中最右边的元素位于其最终排序位置(因为它是数组中最大的元素)。在外循环的第二次迭代之后,数组的最后两个元素处于它们的最终排序位置。等等。因此,我们可以将内循环的迭代次数缩短如下:j &lt; arr.length - 1 - i

  • 如果在任何迭代中都没有执行交换,我们可以中断——数组已经排序。

这些优化并没有提高时间复杂度,如您所说,时间复杂度为 O(n2),但值得考虑,因为类似的优化方法可以帮助加速现实世界排序例程。

需要考虑的几个风格点:

  • 在运算符之间使用空格(除非它们在[]s 中)。
  • 考虑在交换值时使用destructuring assignment 来避免temp 变量。正如 cmets 中所指出的,由于每次交换创建数组对象的开销,这会降低性能,因此应该在生产构建中编译出来(尽管冒泡排序无论如何都不适合生产)。
  • 与分号和垂直空格保持一致。

这是一个可能的重写:

const bubbleSort = arr => {
  for (let i = 0; i < arr.length; i++) {
    let swapped = false;
    
    for (let j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j+1]) {
        [arr[j], arr[j+1]] = [arr[j+1], arr[j]];
        swapped = true;
      }
    }
    
    if (!swapped) { break; }
  }

  return arr;
};

for (let i = 0; i < 10000; i++) {
  const arr = Array(100).fill().map(e => ~~(Math.random() * 30));
  const expected = JSON.stringify(arr.slice().sort((a, b) => a - b));
  const actual = JSON.stringify(bubbleSort(arr.slice()));
  
  if (actual !== expected) {
    throw new Error(`test failed: expected ${expected} but got ${actual}`);
  }
}

console.log("10000 tests passed");

有趣的后续练习:

  • 不用循环,使用递归。
  • 允许用户传递自定义比较器函数,类似于内置的Array#sort。这样,您可以对对象数组进行排序或指定排序顺序。
  • 使用 console.time 将其与其他排序例程进行基准测试(感谢 Medet 的想法)。优化有多大帮助?考虑使用种子随机数进行测试。

【讨论】:

  • 感谢您的回答!您的所有观点都非常有帮助!我对你关于内循环的观点有疑问……循环是如何越界的?我的意思是“j”已经在 j=0 上迭代到小于 arr.length。我在这里错过了一点吗?
  • 抱歉,我找到了重点。该函数正在访问 arr[j+1] 元素!
  • 我们可以比较排序和冒泡排序的速度吗? (console.time 可能吗?)
  • 我可以补充一点,但我认为 OP 明白冒泡排序非常无用,并且可能足以说 Array#sort 将给冒泡排序带来一些可怕的东西。
  • 不要在这里使用解构赋值。是的,它可能会使代码更具可读性,但是您正在编写一个排序算法,每次子迭代创建两个新数组是一个非常糟糕的主意,垃圾收集器会不断启动。仅供参考,在我的 FF 上使用临时变量,我有大约 400 毫秒的时间来完全执行 sn-p,而解构需要 2500 毫秒。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-02
  • 2017-03-11
  • 2012-07-23
  • 2012-01-24
  • 2011-06-02
相关资源
最近更新 更多