算法概念 经常使用排序算法

算法

算法(Algorithm): 是对特定问题求解步骤的一种描述,它是指令的有序序列,其中每一条指令表示一个或者多个操做算法

5个重要特征

有穷性

执行有穷步骤后结束,有穷时间内完成数组

肯定性

每一条指令有确切含义,任何条件下只有惟一的执行路径,相同的输入相同的输出markdown

可行性

一个算法是能行的, 描述的步骤基于已实现的基本运算 执行有限次 实现ide

输入

有0个或者多个输入,刻画运算的对象的初始状况函数

输出

有一个个或者多个输出, 这些输出和输入存在特定关系测试

算法设计的要求:

  • 正确性(语法正确、对于一切合法不管简单或苛刻的输入数据均可以获得知足规格说明要求的结果)
  • 可读性(容易理解、交流、调试和修改,没有隐藏错误易)
  • 健壮性(对非法输入不会产生莫名其妙的输出结果)
  • 效率与低存储量需求,效率指的是算法执行时间存储量指的是算法执行过程当中须要的最大存储空间.

算法效率的度量

过后统计法

经过设计好的测试程序和数据,利用计算机计时器对不一样算法编制的程序的运行时间进行比较,从而肯定算法效率的高低.优化

缺陷:ui

  • 必须依据算法实现编制好测试程序
  • 所得时间的统计依赖计算软硬件环境因素。

事前分析估算

  • 算法采用的策略和方案
  • 问题的规模, n = 1000, n = 10000;
  • 书写的语言,语言级别越高,执行效率越低
  • 编译程序所产生的代码的质量
  • 机器执行指令的速度。

因而可知,抛开这些与计算机硬件,软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模.(所谓的问题输入规模是指输入量的多少(一般用整数量n 表示))spa

一个算法是有控制结构(顺序,分支,循坏)和原操做(固定数据类型的操做),算法时间取决于二者的综合效果。设计

算法时间度量:

对所研究问题(算法类型)基本操做的原操做以及对基本操做重复执行的次数度量。算法的基本操做的重复次数是问题规模的某个函数f(n). 算法时间量度记作

T(n) = O(f(n));

时间复杂度:

随着问题规模的n 的增大,算法执行时间的增加率和f(n)的增加率相同。

T(n) = O(f(n));

频度

某些控制语句的重复的执行的次数,(for, while, do while)。

问题的基本的原操做重复执行次数和算法的执行时间成正比。

常见的时间复杂度:

image.png 经常使用的时间复杂度所耗费的时间从小到大依次是:

O(1) < O(logn) < (n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!);

算法平均复杂度

对全部可能的输入数据集合的指望值

最坏时间复杂度。

分析最坏状况估算执行时间的上线

...
 for(i =2; i <=n; i++){
   for(j =2; j <=i-1; j++){
     ++x;
     a[i][j] = x;
   }
 }
 语句频度:(n - 1)(n-2)/2
 时间平均复杂度T arg(n) = O(n^2);
 最坏时间复杂度T(n) = O(n^2);
复制代码

算法空间复杂度

S(n) = O(f(n));

n 问题的规模 f(n)为语句关于n所占存储空间的函数。 一个上机程序除了需哟啊存储空间寄存自己所用的指令、常数、变量和输入数据外,对数据进行操做的工做单元和存储一些为实现计算所须要的信息和辅助空间.

排序算法

经过特定的算法因式一组或多组数据 按照既定模式进行从新排序

评价标准

  • 稳定性:

两个相同的元素同时出现于某个序列之中,则通过必定的排序算法以后,二者在排序先后的相对位置不发生变化.

注:稳定性是一个特别重要的评估标准。稳定的算法在排序的过程当中不会改变元素彼此的位置的相对次序,反之不稳定的排序算法常常会改变这个次序,这是咱们不肯意看到的

  • 时间复杂度
  • 空间复杂度

排序算法的分类

非线性时间比较类排序:比较,由时间复杂度不能突破(nlogn) 交换排序(冒泡、快速)、插入排序(直接插入、希尔排序)、选择排序(简单选择排序,堆排序), 归并排序(二路归并,多路归并)

线性时间非比较类排序:不比较,突破基于比较排序的时间下界,以线性时间运行 基数排序、桶排序、计数。

内部排序:指的是待排序记录存放在计算机随机存储器进行排序

外部排序:待排序记录数量的数量很大,一次不能够存放所有记录,需对外存访问

image.png

快速排序(Quicksort)

经过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另外一部分的关键字小,则可分别对这两部分记录继续进行递归排序,以达到整个序列有序。 这个关键字是从数列中挑出一个元素,也称为 “基准”(pivot).

实现步骤:

  • 选择一个基准元素target(通常选择第一个数)
  • 将比target小的元素移动到数组左边,比target大的元素移动到数组右边
  • 分别对target左侧和右侧的元素进行快速排序

复杂度

时间复杂度:平均O(nlogn),最坏O(n2),实际上大多数状况下小于O(nlogn)

空间复杂度:O(logn)(递归调用消耗)

稳定性

不稳定

写法1 左开右闭

记录一个索引l从数组最左侧开始,记录一个索引r从数组右侧开始

l<r的条件下,找到右侧小于target的值array[r],并将其赋值到array[l]

l<r的条件下,找到左侧大于target的值array[l],并将其赋值到array[r]

这样让l=r时,左侧的值所有小于target,右侧的值所有小于target

const quickSort = function(array, start , end) {
   if(end - start < 1) return;
   let l = start;
   let r = end;
   const target = array[l];
   while(l < r) {
     while(l < r && array[r] >=  target) {
       r--
     };
     array[l] = array[r];
     while(l < r && array[l] < target) {
      l++
     };
     array[r] = array[l];
   }
   array[l] = target
   quickSort(array, start, l - 1);
   quickSort(array, l+1, end);
 }
 const arr = [4, 1, 5, 7, 3, 9];
 quickSort(arr, 0, 5);
 
复制代码

image.png

写法2

单独开辟两个存储空间leftright来存储每次递归比target小和大的序列

每次递归直接返回left、target、right拼接后的数组

浪费大量存储空间,写法简单

function quickSort(array) {
  if(array.length < 2) {
     return array;
  }
  const target = array[0];
  const left = [];
  const right = [];
  for(let i = 1; i < array.length; i++) {
      if(array[i] < target) {
        left.push(array[i])
      } else {
         right.push(array[i]);
      }
  }
  return quickSort(left).concat([target], quickSort(right));
}
const arr = [4, 1, 5, 7, 3, 9];
quickSort(arr, 0, 5);
复制代码

image.png

插入排序

将左侧序列当作一个有序序列,每次将一个数字插入该有序序列。

插入时,从有序序列最右侧开始比较,若比较的数较大,后移一位。

复杂度

时间复杂度:O(n2)

空间复杂度:O(1)

稳定性

稳定

function insertSort(array) {
  for(let i = 1, len = array.length; i < len; i++) {
     let target = i;
     for(let j = i - 1; j >=0; j--) {
       if(array[target] < array[j]) {
         [array[target], array[j]] = [array[j], array[target]];
         target = j;
       }else break;
     }
  }
}
const arr = [4, 1, 5, 7, 3, 9];
insertSort(arr, 0, 5);

复制代码

image.png

堆排序

建立一个大顶堆,大顶堆的堆顶必定是最大的元素。

交换第一个元素和最后一个元素,让剩余的元素继续调整为大顶堆。

从后往前以此和第一个元素交换并从新构建,排序完成。

复杂度

时间复杂度:O(nlogn)

空间复杂度:O(1)

稳定性

不稳定

function heapSort(array) {
   creatHeap(array);
   for(let i = array.length - 1; i > 0; i--) {
      [array[i], array[0]] = [array[0], array[i]];
      adjust(array, 0, i);
   }
   return array;
}
function creatHeap(array) {
  const len = array.length;
  const start = parseInt(len/2) - 1;
  for(let i = start; i >=0; i--) {
    adjust(array, i, len);
  }
}
// 将第target个元素进行下沉,孩子节点有比他大的就下沉
function adjust(array, target, len) {
   for(let i = 2 * target + 1; i< len; i = 2 * i + 1) {
   // 找到孩子节点中最大的
     if(i + 1 < len && array[i + 1] > array[i]) {
       i = i + 1;
     }
     // 下沉
     if(array[i] > array[target]){
        [array[i], array[target]] = [array[target], array[i]]
     }else {
     break;}
   }
}
const arr = [4, 1, 5, 7, 3, 9];
heapSort(arr);
复制代码

image.png

归并排序

利用归并的思想实现的排序方法。

该算法是采用分治法(Divide and Conquer)的一个很是典型的应用。(分治法将问题分红一些小的问题而后递归求解,而治的阶段则将分的阶段获得的各答案"修补"在一块儿,即分而治之)。

  • 若将两个有序表合并成一个有序表,称为二路归并。

分割:

  • 将数组从中点进行分割,分为左、右两个数组
  • 递归分割左、右数组,直到数组长度小于2

归并

须要合并,那么左右两数组已经有序了。

建立一个临时存储数组temp,比较两数组第一个元素,将较小的元素加入临时数组

若左右数组有一个为空,那么此时另外一个数组必定大于temp中的全部元素,直接将其全部元素加入temp

  • 时间复杂度:O(nlogn)

  • 空间复杂度:O(n)

  • 稳定

解法一

分割数组时直接将数组分割为两个数组,合并时直接合并数组。 缺点:空间复杂度略高,须要复制多个数组

function mergeSort(array) {
  if(array.length < 2) {
    return array;
  }
  const mid = array.length >> 1;
  const front = array.slice(0, mid);
  const end = array.slice(mid);
  return merge(mergeSort(front), mergeSort(end));
 }
 function merge(front, end) {
   const temp = [];
   while(front.length && end.length) {
     if(front[0] < end[0]) {
        temp.push(front.shift())
     } else {
       temp.push(end.shift())
     }
   }
   while(front.length) {
   temp.push(front.shift());
   }
   while(end.length) {
    temp.push(end.shift())
   }
   return temp;
 }

const arr = [4, 1, 5, 7, 3, 9];
mergeSort(arr);
复制代码

image.png

解法2 记录数组的索引,使用left、right两个索引来限定当前分割的数组。

优势:空间复杂度低,只需一个temp存储空间,不须要拷贝数组

function mergeSort(array, left, right, temp) {
      if (left < right) {
        const mid = Math.floor((left + right) / 2);
        mergeSort(array, left, mid, temp)
        mergeSort(array, mid + 1, right, temp)
        merge(array, left, right, temp);
      }
      return array;
    }

    function merge(array, left, right, temp) {
      const mid = Math.floor((left + right) / 2);
      let leftIndex = left;
      let rightIndex = mid + 1;
      let tempIndex = 0;
      while (leftIndex <= mid && rightIndex <= right) {
        if (array[leftIndex] < array[rightIndex]) {
          temp[tempIndex++] = array[leftIndex++]
        } else {
          temp[tempIndex++] = array[rightIndex++]
        }
      }
      while (leftIndex <= mid) {
        temp[tempIndex++] = array[leftIndex++]
      }
      while (rightIndex <= right) {
        temp[tempIndex++] = array[rightIndex++]
      }
      tempIndex = 0;
      for (let i = left; i <= right; i++) {
        array[i] = temp[tempIndex++];
      }
    }
const arr = [4, 1, 5, 7, 3, 9];
mergeSort(arr, 0, 5, []);
复制代码

image.png

冒泡

循环数组,比较当前元素和下一个元素,若是当前元素比下一个元素大,向上冒泡。

这样一次循环以后最后一个数就是本数组最大的数。

下一次循环继续上面的操做,不循环已经排序好的数。

优化:当一次循环没有发生冒泡,说明已经排序完成,中止循环。

  • 时间复杂度:O(n2)

  • 空间复杂度:O(1)

  • 稳定

function bubbleSort(array) {
   for(let j = 0, len = array.length; j < len; j++) {
     let complete = true;
     for(let i = 0; i < len - 1 - j; i++) {
       if(array[i] > array[i+1]) {
          [array[i], array[i+1]] = [array[i+1], array[i]];
          complete = false;
       }
     }
     if(complete) {
     break;
     }
   }
   return array;
 }
const arr = [4, 1, 5, 7, 3, 9];
bubbleSort(arr);
复制代码

image.png

选择排序

每次循环选取一个最小的数字放到前面的有序序列中。

  • 时间复杂度:O(n2)

  • 空间复杂度:O(1)

  • 不稳定

function selectionSort(array) {
  for(let i = 0, len = array.length; i < len -1; i++) {
     let minIndex = i;
     for(let j = i+1; j < len; j++) {
       if(array[j] < array[minIndex]) {
       minIndex = j
       }
     }
     [array[minIndex], array[i]] = [array[i], array[minIndex]];
  }
}
const arr = [4, 1, 5, 7, 3, 9];
selectionSort(arr);
复制代码

image.png

相关文章
相关标签/搜索