2020-11-19 排序算法一(冒泡和快排多种实现)

2020-11-24  本文已影响0人  宇宙区长李小无

主流的排序算法

稳定排序和不稳定排序:有些序列中,可能存在相同的数,如果排序前后他们的先后顺序没有变化,则稳定。

冒泡排序

鸡尾酒排序(终极版冒泡)

上面的算法都有一个问题,那就是对比方向是从左到右,而且我们比较的是右数大于左数就移动,那么最大的数可以一次遍历就移动到边缘,但是对于较小数,我们只交换过一次位置,就不再处理了。就导致我们需要遍历n-1次,才能达到目的。

从左向右可以一次处理最大值,那么最小值就可以考虑从右到左,一次处理,将其移动到最小端边缘。
鸡尾酒排序的过程就类似于钟摆,先从左到右,然后再从右到左...只要有一轮没有发生过位置变化,则说明已经有序

  function bubble4(list: number[]) {
    console.log(list);
    for (let i = 0; i < list.length / 2; i++) {
      let temp = 0;
      let isSorted = true;
      // 从左向右
      for (let j = 0; j < list.length - i - 1; j++) {
        if (list[j] > list[j+1]) {
          temp = list[j];
          list[j] = list[j+1];
          list[j+1] = temp;
          isSorted = false;
        }
      }
      if (isSorted) {
        break;
      }
      isSorted = true;
      for (let j = list.length - i - 1; j > i; j--) {
        if (list[j] < list[j-1]) {
          temp = list[j];
          list[j] = list[j-1];
          list[j-1] = temp;
          isSorted = false;
        }
      }
      if (isSorted) {
        break;
      }
    }
    console.log(list);
  }

快速排序

冒泡排序的时间复杂度达到了O(n^2),快速排序呢?我们先来看一下快速排序的核心概念:

这就是典型的分治法,由于需要遍历所有对象,所有复杂度暂时为O(n),又由于每次分一半,相当于一个长度为n的数列需要分割logn次,所以最终快速排序时间复杂度为O(nlogn)

基准元素

如何找一个合适的基准元素呢?根据上面的过程,我们可以得出,这个基准数如果是数列的中位数,那非常不错,我们每次分割的数列长度会近乎相等,时间复杂度也完美契合O(nlogn)。但是当我们选中的基准数为数列的最大值或者最小值呢?这时分割后只会存在一个队列,导致我们可以分割n次,时间复杂度退化成了O(n^2)。

技巧:一般来说,我们会寻找一个随机数,让其与数列首位的值进行交换,然后将其当作基准数,大多数情况下这样是可以保证我们的分治法得到发挥,但也存在极小概率使得我们选择了一个极值作为基准数。因此快速排序的时间复杂度并不是完全固定的。

元素交换
function quickSort(list: number[]) {
  if (list.length <= 1) {
    return list;
  }
  // 找到一个随机基准数并放到数列首位
  const pivotIndex = Math.floor(Math.random() * list.length);
  const pivot = list[pivotIndex];
  list[pivotIndex] = list[0];
  list[0] = pivot;
  // 初始化左右指针
  let leftIndex = 0;
  let rightIndex = list.length - 1;
  while (leftIndex != rightIndex) {
    // 右侧指针对比并向左移动
    while (leftIndex < rightIndex && list[rightIndex] > pivot) {
      rightIndex--;
    }
    // 左侧指针对比并向右移动
    while (leftIndex < rightIndex && list[leftIndex] <= pivot) {
      leftIndex++;
    }
    // 左右指针都运动一次后交换对应元素
    if (leftIndex < rightIndex) {
      list[leftIndex] = list[leftIndex] ^ list[rightIndex];
      list[rightIndex] = list[leftIndex] ^ list[rightIndex];
      list[leftIndex] = list[leftIndex] ^ list[rightIndex];
    }
  }
  // 遍历结束将基准元素放置数列中间
  list[0] = list[leftIndex];
  list[leftIndex] = pivot;
  if (list.length < 3) {
    return list;
  }
  const leftList = list.slice(0, leftIndex);
  const rightList = list.slice(leftIndex+1);
  // 递归获取结果
  return [...quickSort(leftList), pivot, ...quickSort(rightList)];
}
      // 单边循环法
function quickSortSingleSide(list: number[]) {
  if (list.length <= 1) {
    return list;
  }
  let mark = 0;
  // 基准元素定第一个元素
  const pivot = list[mark];
  for (let i = mark+1; i < list.length; i++) {
    // 从第二个元素开始遍历,如果小于pivot,则指针右移一位,代表比基准数小的数多一位
    if (list[i] < pivot) {
      mark++;
      // 将小于基准数的数与mark的位置的数进行交换(保证mark位置及左边的元素一定小于pivot)
      const markValue = list[mark];
      list[mark] = list[i];
      list[i] = markValue;
    }
  }
  // 交换pivot和mark位置的数,形成以pivot为界限的左小右大有序数列
  list[0] = list[mark];
  list[mark] = pivot;
  return [...quickSortSingleSide(list.slice(0, mark)), pivot, ...quickSortSingleSide(list.slice(mark+1))];
}
栈替换递归

我们说过,大多数递归操作,都可以用栈来替换其实现,每次递归,相当于往栈内压入了一个方法的执行域,而当方法返回时,就相当于出栈。

数组实现栈:
function sortByStack(list: number[]) {
  // 定义存放栈为数组
  const stack: {start: number, end: number}[] = [];
  // 定义栈顶元素
  const root = { start: 0, end: list.length - 1 };
  // 入栈
  stack.push(root);
  // 遍历条件为栈内有元素
  while (stack.length > 0) {
    // 执行一次遍历,需要将栈顶出栈
    const stackPop = stack.pop();
    // 获取该次遍历得到的mark标记位置
    const pivotIndex = getPivotIndex(list, stackPop.start, stackPop.end);
    // 如果mark左侧还有多于1个元素,那么说明左侧的元素还可以再遍历,将其压入栈内
    if (stackPop.start < pivotIndex - 1) {
      stack.push({
        start: stackPop.start,
        end: pivotIndex - 1
      });
    }
    // 如果mark右侧还有多于1个元素,那么说明右侧的元素还可以再遍历,将其压入栈内
    if (stackPop.end > pivotIndex + 1) {
      stack.push({
        start: pivotIndex + 1,
        end: stackPop.end
      });
    }
  }
  return list;
}
// 直接在此方法内修改数列元素的位置,并返回mark位置索引
function getPivotIndex(list: number[], start: number, end: number): number {
  const pivot = list[start];
  let mark = start;
  for (let i = start + 1; i <= end; i++) {
    if (list[i] < pivot) {
      mark++;
      const markValue = list[mark];
      list[mark] = list[i];
      list[i] = markValue;
    }
  }
  list[start] = list[mark];
  list[mark] = pivot;
  return mark;
}
链表实现栈

一直用数组实现,虽然数组比较直观,但是链表也不是吃素的,需要注意的是:

function sortByLinkList(list: number[]) {
  const root = { start: 0, end: list.length - 1 };
  // 头节点
  let linkList: any = {
    data: {
      start: root.start,
      end: root.end
    },
    next: null
  };
  // 头节点不为空则继续遍历
  while (linkList.data) {
    console.log("链表", linkList);
    // 获取头节点
    const linkListEnd = linkList.data;
    // 表头出栈
    linkList = linkList.next || {};
    const pivotIndex = getPivotIndex(list, linkListEnd.start, linkListEnd.end);
    if (linkListEnd.start < pivotIndex - 1) {
      const newLinkList = {
        data: {
          start: linkListEnd.start,
          end: pivotIndex - 1
        },
        next: linkList
      };
      // 每次入栈相当于往表头插入新的节点,将其变为头节点
      linkList = newLinkList;
    }
    if (linkListEnd.end > pivotIndex + 1) {
      const newLinkList = {
        data: {
          start: pivotIndex + 1,
          end: linkListEnd.end
        },
        next: linkList
      };
      // 每次入栈相当于往表头插入新的节点,将其变为头节点
      linkList = newLinkList;
    }
  }
  return list;
}

下节见吧!

上一篇下一篇

猜你喜欢

热点阅读