美文网首页
2020-11-19 排序算法一(冒泡和快排多种实现)

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

作者: 宇宙区长李小无 | 来源:发表于2020-11-24 15:56 被阅读0次

    主流的排序算法

    • 时间复杂度为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 排序算法一(冒泡和快排多种实现)

          本文链接:https://www.haomeiwen.com/subject/tfbkiktx.html