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

主流的排序算法

  • 时间复杂度为O(n^2):
    冒泡排序
    选择排序
    插入排序
    希尔排序(介于O(n^2)与O(nlogn)之间的)

  • 时间复杂度为O(nlogn):
    快速排序
    归并排序
    堆排序

  • 时间复杂度为线性的:
    计数排序
    桶排序
    基数排序

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

冒泡排序

  • 乞丐版:双重循环,滴水不漏;
    function bubble1(list: number[]) {
      console.log(list);
      for (let i = 0; i < list.length - 1; i++) {
        for (let j = 0; j < list.length - 1 - i; j++) {
          let tem = 0;
          if (list[j] > list[j+1]) {
            tem = list[j];
            list[j] = list[j+1];
            list[j+1] = tem;
          }
        }
      }
      console.log(list);
    }
    
  • 白银版:isSorted标记,循环过程中判断有没有位置交换,如果没有,则直接退出了。
    function bubble2(list: number[]) {
      console.log(list);
      for (let i = 0; i < list.length - 1; i++) {
        let isSorted = true;
        for (let j = 0; j < list.length - 1 - i; j++) {
          let temp = 0;
          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);
    }
    
  • 黄金版:界定有序区,避免无效对比,isSorted配合sortBorder,发生位置交换就记下该位置索引,最后一次交换的位置一定是有序区的边界。
    function bubble3(list: number[]) {
      console.log(list);
      let lastIndex = list.length - 1;
      let sortBorder = lastIndex;
      for (let i = 0; i < list.length - 1; i++) {
        let isSorted = true;
        for (let j = 0; j < sortBorder; j++) {
          let temp = 0;
          if (list[j] > list[j+1]) {
            temp = list[j];
            list[j] = list[j+1];
            list[j+1] = temp;
            lastIndex = j;
            isSorted = false;
          }
        }
        sortBorder = lastIndex;
        if (isSorted) {
          break;
        }
      }
      console.log(list);
    }
    
鸡尾酒排序(终极版冒泡)

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

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

元素交换
  • 双边循环法
    • 1、找到基准元素pivot(默认为第一个元素list[0]),遍历过程中,存在两个指针,一个left(初始值为0),一个right(初始值为list.length - 1);
    • 2、在left < right的条件(因为等于则说明该次遍历完成)下,-①-从right开始,判断right指针的元素是否大于pivot,如果大于,则right指针左移(right--),直到right元素小于等于pivot,此时目标移至left指针,如果left指针元素小于等于pivot(初始肯定是等于的),则left指针右移(left++),直至left指针元素大于pivot,此时,left指针元素大于pivot,而right指针元素小于pivot,由于我们想要数列从左往右呈现从小到大的顺序,所以交换left和right指针的值。再从上述的-①-处开始判断。直到left指针与right指针重合,此时交换指针元素和pivot元素位置。
    • 3、第二步完成后,我们得到了一个以pivot值为分界线,左侧小于pivot,右侧大于pivot的数列,根据分治法,我们将左侧和右侧分别再当成一个数列进行1、2步,直到左侧右侧数列不可再分。
    • 4、递归进行,将结果拼接起来,即可形成一个有序数列。
      第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)];
}
  • 单边循环法
    该方法比较简单
    • 1、找一个基准数pivot,一般直接取第一个即可,也可以随机找一个,然后放置到数列首位;
    • 2、定义一个mark指针,从一个方向进行遍历,遇到比pivot小的数,mark右移一位,代表比pivot 小的数多一个,然后交换mark位置的值和当前遍历的值,这是为了保证mark位置及左边的元素一定是小于pivot的,因为我们遇到比pivot大的数是按兵不动的。
    • 3、当数列遍历完成后,交换mark位置的值和pivot。形成一个以pivot为基准,左小右大的数列。
    • 4、分治法,递归左右数列得到结果。
      // 单边循环法
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;
}

下节见吧!

你可能感兴趣的:(2020-11-19 排序算法一(冒泡和快排多种实现))