javaScript知识点汇总(一)

359 阅读23分钟

前端面试知识点

基本类型

基础
  1. undefined 不是关键词可能被修改 (指局部环境下) (现代浏览器已经修改undefined的 non-writable non-configurable的值 全局环境无法被修改)

  2. null 是关键词

  3. void 0 为 unfefined

  4. String 最大长度为 2^53 - 1 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码 JavaScript 中的字符串是永远无法变更的

  5. Number 类型中有效的整数范围是 -0x1fffffffffffff 至 0x1fffffffffffff

  6. 同样根据浮点数的定义,非整数的 Number 类型无法用 == 或 === (所以 0.1+0.2 == 0.3 //false) Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON // true 检查等式左右两边差的绝对值是否小于最小精度

  7. Number、String 和 Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。 使用 new Number(3) 得到的是对象

  8. 在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。

装箱拆箱

​ 在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。

ToPrimitive 为内部函数,在将对象转换成原始值则会调用toPrimitive(input,preferedType?)

其中 input 为输入的值 preferedType是期望转换的类型

基本流程如下

  1. 如果input是原始值(原始类型),直接返回这个值
  2. 如果input是对象,则调用input。valueOf() 返回结果
  3. Else 调用 input.toString() 返回结果
  4. 否则抛出错误

如果转换的类型是String 2和3 顺序会变化

在操作符中,==,排序运算符,加减乘除,在对非原始值进行操作时,都会调用内部的toPrimitive()方法

tips: 加上操作符会将preferedType看成Number

[] + [] = ""
[] + {} = [object object]
{} + [] = 0

装箱:将原始类型转成对象(原始类型有七种 boolean, number, string, undefined,null,bigInt,symbol)

var a=10 ,b="javascript" , c=true;
var o_a=new Number(a);

​ 拆箱: 将引用类型对象转换为对应的值类型对象,它是通过引用类型的valueOf()或者toString()方法来实现的。如果是自定义的对象,你也可以自定义它的valueOf()/tostring()方法,实现对这个对象的拆箱。

var a=10;

var o_a=new Number(a);

var b=o_a.valueOf();

js运行时

js属性

Javascript 数据属性
  • Value: 属性值
  • writable: 属性是否能被赋值
  • enumerable: 觉得for in 能否枚举该属性
  • configurable: 决定该属性能否被删除或被修改特征值
Javascript 访问属性
  • getter: 函数或 undefined, 在读取属性值被调用。

  • setter: 函数或undefined,在设置属性值时被调用。

  • enumerable: 决定for in能否枚举该属性

  • configurable: 决定该属性能否被删除或改变特征值

操作方法

Object.getOwnPropertyDescripter
var a = { b: 1}
a.c = 2

Object.getOwnPropertyDescriptor(a,"b") 
// {value: 1, writable: true, enumerable: true, configurable: true} 默认都为true

Object.getOwnProertyDescriptor(a,"c")
// {value: 2, writable: true, enumerable: true, configurable: true}

Object.defineProperty 定义属性
var o = { a: 1 }
Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true})

javascript 类

Javascript 内置函数
  • object.create 根据指定的原型创建新对象,原型可以是nulll。
  • object.getPrototypeOf 获取一个对象的原型。
  • object.setPrototypeOf 设置一个对象的原型。

在 ES3 和之前的版本,JS 中类的概念是相当弱的,它仅仅是运行时的一个字符串属性。

在 ES5 开始,[[class]] 私有属性被 Symbol.toStringTag 代替

使用 Symbol.toStringTag 来自定义 Object.prototype.toString 的行为:

var o = { [Symbol.toStringTag]: "myObject" }
console.log(o + "");

javascript 对象分类

分类
  • 宿主对象(Host Objects): 由 javaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定

    • window (固有)
    • 可创建的 document.createElemenet 创建的dom对象
    • 构造器 比如 new Image 创建img 元素
  • 内置对象(Built-in Object): 由 JavaScript 语言提供的对象。 (运行时runtime)

    • 固有对象(Intrinsic Object): 由标准规定,随着JavaScript运行时创建而自动创建的对象实例

      • 基础类库用到较多 150+固有对象
        • 举例如: ToPrimitive( input [, PreferredType]) 装箱拆箱
        • ToBoolean、ToNumber等
    • 原生对象(Native Object): 可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。

    • 普通对象(Ordinary Object): 由{}语法、Object 构造器或者class关键词定义类创建的对象,它能够被原型继承

    • Tips

      function a() {}
      a() // 包含[[call]]私有字段对象 表示可以作为函数被调用
      new a() // 包含 [[construct]] 构造器对象,可以作为构造器被调用
      // 对于部分宿主和内置对象两则有时效果不相同 比如 date image
      
      // 实现 私有变量
      function cls() {
        this.a = 100;
        return {
          getValue: ()=> this.a
        }
      }
      
      var b = new cls;
      b.getValue() // 100
      // a的值在外面无法访问到
      
      //如何创建一个对象
      //字面量创建
      var a = {}
      // dom api
      var d = document.createElement('p')
      // 内置对象
      var e = Object.create(null)
      // 装箱
      var f = Object(null)
      
      

js 执行机制

setTimeout会单独生产一个新的宏观任务,在上一个宏观任务执行完毕后执行。

执行上下文(在堆栈中)

ES3

  • scope:作用域,也常常被叫做作用域链。
  • variable object:变量对象,用于存储变量的对象。
  • this value:this 值。

ES5

  • lexical environment:词法环境,当获取变量时使用。
  • variable environment:变量环境,当声明变量时使用
  • this value:this 值。

ES2018

  • lexical environment:词法环境,当获取变量或者 this 值时使用。
  • variable environment:变量环境,当声明变量时使用。
  • code evaluation state:用于恢复代码执行位置。
  • Function:执行的任务是函数时使用,表示正在被执行的函数。
  • ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
  • Realm:使用的基础库和内置对象实例。
  • Generator:仅生成器上下文有这个属性,表示当前生成器。

this

  • this 是执行上下文中很重要的一个组成部分。同一个函数调用方式不同,得到的 this 值也不同

  • 普通函数的 this 值由“调用它所使用的引用”决定,其中奥秘就在于:我们获取函数的表达式,它实际上返回的并非函数本身,而是一个 Reference 类型(记得我们在类型一章讲过七种标准类型吗,正是其中之一)。Reference 类型由两部分组成:一个对象和一个属性值。不难理解 o.showThis 产生的 Reference 类型,即由对象 o 和属性“showThis”构成。

  • bind 、call、apply 函数通过函数调用时传入 this

  • javascript 标准 定义了 [[thisMode]]私有属性。[[thisMode]]有三个取值

    • lexical : 表示从上下文中找this, 这对应了箭头函数

    • Global: 表示当this为undefined时,取全局对象,对应了普通函数。

    • Strict: 当严格模式时使用,this严格按照调用时传入的值,可能为null或者undefined。

      Class 默认按照strict模式执行

      "use strict"
      function showThis() {
        console.log(this);
      }
      var o = {
        showThis: showThis
      }
      
      showThis(); // undefined 严格模式下 this按照传入的值 所有未定义
      o.showThis(); // o 
      

      函数创建新的执行上下文中的词法环境记录时,会根据 [[thisMode]] 来标记新纪录 [[ThisBindingStatus]] 私有属性。

      代码执行遇到 this 时,会逐层检查当前词法环境记录中的 [[ThisBindingStatus]],当找到有 this 的环境记录时获取 this 的值。

      这样的规则的实际效果是,嵌套的箭头函数中的代码都指向外层 this。

箭头函

  • 箭头函数不会绑定 this, 会捕获其所在的上下文this值,作为自己的this。嵌套的箭头函数中的代码都指向外层 this
  • 箭头函数是匿名函数,不能作为构造函数,不能使用new
  • 箭头函数不绑定arguments ,取而代之用reset参数...解决
  • 箭头函数通过call() 或apply 方法调用一个函数时,只传入了一个参数,对this并没有影响
Completion 类型

Completion Record 用于描述异常、跳出等语句执行过程

Completion Record 表示一个语句执行完之后的结果,它有三个字段:

  • [[type]]表示完成的类型,有 break continue return throw 和 normal 几种类型;
  • [[value]] 表示语句的返回值,如果语句没有,则是 empty;
  • [[target]] 表示语句的目标,通常是一个 JavaScript 标签。

JavaScript 正是依靠语句的 Completion Record 类型,方才可以在语句的复杂嵌套结构中,实现各种控制。

普通语句执行后,会得到 [[type]] 为 normal 的 Completion Record,JavaScript 引擎遇到这样的 Completion Record,会继续执行下一条语句。

这些语句中,只有表达式语句会产生 [[value]],当然,从引擎控制的角度,这个 value 并没有什么用处。

var i = 1 // 该语句 [[type]]:noraml  [[value]]: empty  [[target]]: empty
// undefined

{//语句块
  var i = 1; // [[type]]:noraml  [[value]]: empty  [[target]]: empty
  return i; // [[type]]:return  [[value]]: 1  [[target]]: empty
} // return , 1, empty

//控制型语句

穿透: 被跳过 | 消费:被执行 | 特殊处理:特殊处理

[[target]]

实际上,任何 JavaScript 语句是可以加标签的,在语句前加冒号即可:

firstStatement: var i = 1;

大部分时候,这个东西类似于注释,没有任何用处。唯一有作用的时候是:与完成记录类型中的 target 相配合,用于跳出多层循环。

outer: while(true) { 
  inner: while(true) {
    break outer; 
  } 
} 
console.log("finished")

在 JavaScript 中,我们把不带控制能力的语句称为普通语句。

Statement completion 扩展

因为 JavaScript 语句存在着嵌套关系,所以执行过程实际上主要在一个树形结构上进行, 树形结构的每一个节点执行后产生 Completion Record,根据语句的结构和 Completion Record,JavaScript 实现了各种分支和跳出逻辑。

Js 词法

词法规定了语言的最小语义单元:token

词法

  • WhiteSpace 空白字符
  • LineTerminator 换行符
  • Comment 注释
  • Token 词
    • IdentifierName 标识符名称,典型案例是我们使用的变量名,注意这里关键字也包含在内了。(var let...)
    • Punctuator 符号,我们使用的运算符和大括号等符号。
    • NumericLiteral 数字直接量,就是我们写的数字。
    • StringLiteral 字符串直接量,就是我们用单引号或者双引号引起来的直接量。
    • Template 字符串模板,用反引号` 括起来的直接量。

js语法

脚本和模块

首先,JavaScript 有两种源文件,一种叫做脚本,一种叫做模块。这个区分是在 ES6 引入了模块机制开始的,在 ES5 和之前的版本中,就只有一种源文件类型(就只有脚本)。

脚本是可以由浏览器或者 node 环境引入执行的,而模块只能由 JavaScript 代码用 import 引入执行

从概念上,我们可以认为脚本具有主动性的 JavaScript 代码段,是控制宿主完成一定任务的代码;而模块是被动性的 JavaScript 代码段,是等待被调用的库。

现代浏览器可以支持用 script 标签引入模块或者脚本,如果要引入模块,必须给 script 标签添加 type=“module”。如果引入脚本,则不需要 type。


<script type="module" src="xxxxx.js"></script>

export 有一种特殊的用法,就是跟 default 联合使用。export default 表示导出一个默认变量值,它可以用于 function 和 class。这里导出的变量是没有名称的,可以使用import x from "./a.js"这样的语法,在模块中引入。

  • require是运行时调用,所以require理论上可以运用在代码的任何地方
  • require是赋值过程,其实require的结果就是对象、数字、字符串、函数等,再把require的结果赋值给某个变量
  • import是编译时调用
    • import是解构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require

函数体

函数体其实也是一个语句的列表。跟脚本和模块比起来,函数体中的语句列表中多了 return 语句可以用。

// 普通函数体
function foo(){ 
  //Function body
}
// 异步函数体
async function foo() {
  // todo
}
// 生成器函数体
function *foo() {
  ....
}
// 异步生成器函数体
async function *foo() {
  ...
}

预处理

var声明

var 声明永远作用于脚本、模块和函数体这个级别,在预处理阶段,不关心赋值的部分,只管在当前作用域声明这个变量。

var 会穿透一切语句结构如 for if

function声明

function 声明出现在 if 等语句中的情况有点复杂,它仍然作用于脚本、模块和函数体级别,在预处理阶段,仍然会产生变量,它不再被提前赋值:

console.log(foo);
if(true) { 
  function foo(){ }
}
// undefined
class声明

class 的声明作用不会穿透 if 等语句结构,所以只有写在全局环境才会有声明作用

指令序言机制

脚本和模块都支持一种特别的语法,叫做指令序言(Directive Prologs)。

这里的指令序言最早是为了 use strict 设计的,它规定了一种给 JavaScript 代码添加元信息的方式。

"use strict"; // 指令序言
function f(){ 
  console.log(this);
};
f.call(null); // null

语句块

循环语句

while , do while

for in 循环

for in 循环枚举对象的属性,这里体现了属性的 enumerable 特征。

let o = { a: 10, b: 20}

Object.defineProperty(o, "c", {enumerable:false, value:30})

for(let p in o)
  console.log(p);

for of 循环和 for await of 循环

iterator 机制,我们可以给任何一个对象添加 iterator,使它可以用于 for of 语句

let o = { 
  [Symbol.iterator]:() => ({ 
    _value: 0, next(){ 
      if(this._value == 10) return { done: true } 
      else return { value: this._value++, done: false };
    } 
  })
}
for(let e of o) console.log(e);// 0 1 2 3 4 5 6 7 8 9

generator function 定义iterator

function* foo() {
  yield 0;
  yield 1;
  yield 2;
  yield 3;
}
for(let e of foo())
  console.log(e) //  0 1 2 3

异步生成器函数

表达式语句

表达式语句实际上就是一个表达式,它是由运算符连接变量或者直接量构成的

PrimaryExpression 主要表达式

表达式的原子项:Primary Expression。它是表达式的最小单位,它所涉及的语法结构也是优先级最高的。

Primary Expression 包含了各种“直接量”,直接量就是直接用某种语法写出来的具有特定类型的值。我们已经知道,在运行时有各种值,比如数字 123,字符串 Hello world,所以通俗地讲,直接量就是在代码中把它们写出来的语法。

任何表达式加上圆括号,都被认为是 Primary Expression,这个机制使得圆括号成为改变运算优先顺序的手段。

( a + b )
MemberExpression 成员表达式

Member Expression 通常是用于访问对象成员的。它有几种形式:

a.b;
a["b"];
new.target;
super.b;

Member Expression 最初设计是为了属性访问的,不过从语法结构需要,以下两种在 JavaScript 标准中当做 Member Expression:

f`a${b}c`;

这是一个是带函数的模板,这个带函数名的模板表示把模板的各个部分算好后传递给一个函数。

new Cls();

另一个是带参数列表的 new 运算,注意,不带参数列表的 new 运算优先级更低,不属于 Member Expression。

NewExpression NEW 表达式

这种非常简单,Member Expression 加上 new 就是 New Expression(当然,不加 new 也可以构成 New Expression,JavaScript 中默认独立的高优先级表达式都可以构成低优先级表达式)。

CallExpression 函数调用表达式

除了 New Expression,Member Expression 还能构成 Call Expression。它的基本形式是 Member Expression 后加一个括号里的参数列表,或者我们可以用上 super 关键字代替 Member Expression。

a.b(c);
super();
a.b(c)(d)(e);
a.b(c)[3];
a.b(c).d;
a.b(c)`xyz`;
LeftHandSideExpression 左值表达式 (条件表达式)

New Expression 和 Call Expression 统称 LeftHandSideExpression,左值表达式。

左值表达式就是可以放到等号左边的表达式。

a() = b

这样的用法其实是符合语法的,只是,原生的 JavaScript 函数,返回的值都不能被赋值。因此多数时候,我们看到的赋值将会是 Call Expression 的其它形式,如:

a().c = b;

左值表达式最经典的用法是用于构成赋值表达式。

AssignmentExpression 赋值表达式

AssignmentExpression 赋值表达式也有多种形态,最基本的当然是使用等号赋值:

a = b

这里需要理解的一个稍微复杂的概念是,这个等号是可以嵌套的:

a = b = c = d

这样的连续赋值,是右结合的,它等价于下面这种:

a = (b = (c = d))
Expression 表达式

赋值表达式可以构成 Expression 表达式的一部分。在 JavaScript 中,表达式就是用逗号运算符连接的赋值表达式。

在 JavaScript 中,比赋值运算优先级更低的就是逗号运算符了。我们可以把逗号可以理解为一种小型的分号。

a = b, b = 1, null;

逗号分隔的表达式会顺次执行,就像不同的表达式语句一样。“整个表达式的结果”就是“最后一个逗号后的表达式结果”。比如我们文中的例子,整个“a = b, b = 1, null;”表达式的结果就是“,”后面的null。

更新表达式 UpdateExpression

左值表达式搭配 ++ -- 运算符,可以形成更新表达式。

-- a;
++ a;
a --;
a ++;

更新表达式会改变一个左值表达式的值。分为前后自增,前后自减一共四种。

一元运算表达式 UnaryExpression
delete a.b;
void a;
typeof a;
- a;
~ a;
! a;
await a;

它的特点就是一个更新表达式搭配了一个一元运算符。

乘方表达式 ExponentiationExpression

乘方表达式也是由更新表达式构成的。它使用**号。

++ i ** 30
2 **30 //正确
-2 ** 30 //报错
// ** 运算为右结合
乘法表达式 MultiplicativeExpression
x * 2;
10 % 2;
10 / 2;
加法表达式 AdditiveExpression
+ 
-
移位表达式 ShiftExpression

移位运算把操作数看做二进制表示的整数,然后移动特定位数。所以左移 n 位相当于乘以 2 的 n 次方,右移 n 位相当于除以 2 取整 n 次。

<< 向左移位
>> 向右移位
>>> 无符号向右移位

普通移位会保持正负数。无符号移位会把减号视为符号位 1,同时参与移位:

-1 >>> 1 // 2^31
关系表达式 RelationalExpression

移位表达式可以构成关系表达式

<=
>=
<
>
instanceof
in
相等表达式 EqualityExpression
a instanceof "object" == true
位运算表达式

位运算表达式含有三种:

  • 按位与表达式 BitwiseANDExpression
  • 按位异或表达式 BitwiseANDExpression
  • 按位或表达式 BitwiseORExpression
逻辑与表达式和逻辑或表达式
  • || &&
条件表达式 ConditionalExpression

条件表达式由逻辑或表达式和条件运算符构成,条件运算符又称三目运算符,它有三个部分,由两个运算符?和:配合使用。

condition ? branch1 : branch2

算法

分类

基础概念
  • 原地排序: 指空间复杂度是o(1)的排序算法
  • 稳定性:经过排序后,相等元素的原有先后顺序不变
冒泡排序 bubble sort

算法思想:冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行⽐较,看是否满足大小关系要求,如果不满⾜就 重复n次,完成n个数据的排序

// 冒泡排序 a表示数组, n表示数组大小
function bubbleSort(a,n) {
  if(n <= 1) return
  for(let i = 0;i < n; ++i) {
    // 提前退出冒泡排序循环的标志位
    let flag = false
    for (let j = 0; j < n -i -1; ++j) {
      if(a[j] > a[j+1]) { // 交换
        	[a[j],a[j+1]] = [a[j+1],a[j]]
	        flag = true
      }
    }
    if(!flag) break // 没有数据交换,提前退出
  }
} 

稳定性:冒泡过程中只有交换才会改变两个元素的前后顺序,为了了保证算法的稳定性,当相邻两个元素大小相等时,我们不做交换,相同 顺序不会发⽣改变,所以是稳定的排序算法。

空间复杂度: 冒泡过程只涉及相邻元素的交换操作,只需要常量量级的临时空间,所以空间复杂度是O(1),是原地排序算法。

时间复杂度:

  • 最好情况是已经排好序,只需要1次冒泡,时间复杂度O(n),

  • 最坏情况是倒序排列的情况,需要进行n次冒泡,时间复杂度是O(n2)

  • 平均情况: 通过有序度和逆序度分析

  • 冒泡排序包含2个操作: 比较交换,每交换一次,有序度就+1,总交换次数是确定的,即是逆序度,也就是 n*(n-1)/2初始有序 最好情况交换次数0,最坏情况交换次数n*(n-1)/2,平均情况下交换次数取两者平均值n*(n-1)/4,而⽐较操作肯定比交换次数多, ⽽复杂度上限是O(n^2),所以平均情况的时间复杂度O(n^2)

    Tips: 有序度逆序度: 对于一个不完全有序的数组,如4、5、6、3、2、1,有序元素对为3个(4,5)、(4,6)和(5、6) 对于⼀一个完全有序的数组,如1、2、3、4、5、6,有序度就是n*(n-1)/2,也就是15,称为满有序度; 关于这3个概念,有一个公式:逆序度=满有序度-有序度,排序的过程就是增加有序度,减少逆序度的过程,最后达到满有序度 )

插入排序 insertion sort

对于⼀个有序数组,往里面添加一个新元素,遍历该数组,找到需要插入的位置,将其插入。

算法思想: 把数组中数据分为2个区间,已排序区未排序区。初始已排序区只有1个元素,就是数组的第⼀个元素。 核⼼思想是取未排序区的元素,在已排序区中找到合适的插入位置将其插入,重复这个过程,直到未排序区的元素为空,算法结束 。

// 插入排序, a表示数组,n表示数组大小
function insertionSort(a,n) {
  if(n <= 1) return
  for (let i = 1;i < n; ++i) {
    let value = a[i]
    let j = i - 1
    while(j >= 0 && a[j] > value) {
      a[j+1] = a[j] // 数据迁移 
      j--
    }
    a[j+1] = value // 插入数据
  }
  
}

性能分析

  • 空间复杂度 O(1) 原地排序
  • 稳定性: 前后顺序不变,是稳定排序
  • 时间复杂度
    • 最好情况是O(n)
    • 最坏情况是倒叙,O(n^2)
    • 循环执行n次插入操作,平均复杂度O(N^2)
选择排序 selection sort

算法思想: 思路和插⼊排序类似,也分已排序区间未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区

// 选择排序 a表示数组 n表示数组大小
function selectionSort( a, n) {
  if	(n <= 1) return;
  for (let i = 0; i < n - 1; ++i) {
  	let minIndex = i;
    for	(let j = i + 1;j < n; ++j) {
      if (a[j] < a[minIndex]) {
        minIndex = j;
      }
    }
    [a[i], a[minIndex]] = [a[minIndex], a[i]] // 交换
  }
}

性能分析:

  • 空间复杂度O(1)
  • 时间复杂度: 最好最坏和平均情况的时间复杂度都是O(n^2)
  • 稳定性:由于最小值会和前面的元素交换位置,故是非稳定排序
插入排序优化版: 希尔排序 shell sort

算法思想:

  • 选取一个步长gap(一般初次增量取gap=n/2),对全部元素进⾏分组,分组的依据是所有距离为gap的倍数的元素分为同一组 a[0-9], gap=10/2=5, 所以第一组a[0],a[5],第⼆组a[1],a[6],第三组a[2],a[7],第四组a[3],a[8],第五组a[4],a[9]
  • 对分好的组,在组内进行插⼊排序
  • 然后取下⼀个gap = gap/2,继续分组并组内进行插入排序
  • 重复以上步骤,直到gap=1,即所有元素在同一个组内,再进行插⼊排序,完成排序。

归并排序 merge sort

算法思想: 分治。要排序⼀个数组,先把数组从中间分成前后2部分,然后对前后部分别排序,再讲排序好的两部分结合在一起 。

性能分析:

  • 空间复杂度: 每次合并都要申请额外的内存空间,所有空间复杂度是O(n)
  • 稳定性: 合并国产中每次元素在和合并前后顺序不变- 稳定排序
  • 时间复杂度: O(nlogn)
快速排序 quick sort

算法思想:

待排序数组A[p..r],选择pr之间的任意⼀个数据作为pivot(分区点),遍历pr之间的数据,将小于pivot的放在左边,⼤于pi 边,pivot放中间。数组被分成3部分,前面A[p..q-1]都是⼩于pivot的,中间是pivot,后⾯面A[q+1..r]都是大于pivot的。然后再根据分治 归排序A[p..q-1]A[q+1..r],直到区间缩小为1,说明排序完成。

利用递归思想

quick_sort(p..r) = quick_sort(p..q-1) + quick_sort(q+1..r)
// 终止条件;
p >= r
// 快速排序 a是数组 n表示数组大小
quick_sort(a, n){
  quick_sort_c(a, 0, n-1)
}
// 递归 p r 为下标
quick_sort_c(a,p,r) {
  if (p >= r) return;
  q = partition(a, p, r)
  quick_sort_c(a, p ,q-1)
  quick_sort_c(a,q+1,r)
}

partition函数实现思路

随机选择一个元素作为pivot(一般情况下,可以选择p到r区间的最后一个元素),然后对A[p...r]分区,函数返回pivot下标。如果不考虑内容消耗,可以临时申请2个数组x和y来帮助完成partition功能,只需要遍历A[p...r],把小于pivot的元素拷贝到数组x,把大于pivot的元素拷贝到s数组y,最后再把x中元素,pivot元素,y中元素按顺序拷贝到A[p...r]即可。注意这时排序就不再是原地排序了。

为了了让partition函数不占用额外的内存空间,我们需要在A[p..r]的原地完成分区操作,具体方法如下

处理类似选择排序,假设pivot=A[r],通过游标iA[p..r-1]分成2部分,A[p..i-1]中的元素都⼩于pivot,叫它"已处理理区间",把A[i...r-1]为"未处理理区间"。每次从未处理理区间A[i..r-1]中取⼀个元素A[j],与pivot对比,如果⼩于pivot,则将其加⼊到已处理区间的末尾,也就是A[i] 数组中插⼊数据导致的数据搬移,选择将A[i]A[j]交换,这样就可以保证O(1)时间复杂度下内将A[j]放到下标为i的位置。

性能分析:

  • 空间复杂度O(1) 原地排序

  • 稳定性: 非稳定排序

  • 时间复杂度:O(nlongn)