一、什么是数组
1.1 数组
数组 数据结构是由相同类型的集合所组成的数据结构,分配一块连续的内存来存储。利用元素的索引(index)可以计算出该元素对应的存储地址。
由上述定义中我们可以提取两个重点内容
- 相同数据类型的集合(同一类型元素所需存储空间大小一致,所以我们可以很方便的利用元素的索引来计算出元素所在的位置)
- 分配一块连续的内存来存储
1.2 javascript中的数组
1.2.1 javascript中的数组形势
从我们目前接触到的javascript的数组,我们可以看出来诸多不同,它并不严格遵循上述数组定义的方法
let arr = [1,'abc',['hehe']] //数组存储不同类型的数据
console.log(arr.length)
arr[arr.length] = 'hello'
console.log(arr.length) // 数组长度随意更改 [1, 'abc', Array(1), 'hello']
1.2.2 javascript数组:快数组和慢数组
快数组:在连续内存中存放数据
慢数组:HashTable结构,一种典型的字典形式。
在v8引擎中,直接创建数组默认的是创建快数组,会为数组直接开辟一块连续的内存空间
在这我们只了解相关概念即可,若想深入研究数组在js中的存储形式,可参照深入理解 JS 数组
二、数组常用的方法
2.1 会修改原数组
| 方法名 | 功能 |
|---|---|
| push | 在最后一位新增一或多个数据,返回长度 |
| pop | 删除最后一位,并返回删除的数据 |
| unshift | 在第一位新增一或多个数据,返回长度 |
| shift | 删除第一位,并返回删除的数据 |
| reverse | 反转数组,返回结果 |
| sort | 排序(字符规则),返回结果 |
| splice | 删除指定位置并替换,返回删除的数据 |
var aa = [1,2,3,4,5,6];
console.log(aa.splice(4)); //[5,6] 返回删除后的数组
aa; // [1,2,3,4]
console.log(aa.splice(2,2)); //[3,4] 从第二位起删除两个元素
aa; //[1,2]
console.log(aa.splice(1,0,7,8)); //[]从第一位起删除0个元素,添加7,8到原数组
aa;//[1,7,8,2]
2.2 不会修改原数组
| 方法名 | 功能 |
|---|---|
| concat | 可以合并一个或多个数组,会返回合并数组之后的数据 |
| join | 将数组转为字符串并返回转化的字符串数据 |
| slice | 截取指定位置的数组,并且返回截取的数组 |
| toString | 将数组作为字符串返回 |
| valueOf | 和toString类似,将数组作为字符串返回 |
| forEach | 对数组中的每个元素运行给定的函数,这个方法没有返回值 |
| map | 对数组中的每个元素运行给定的函数,返回每次函数调用的结果组成的数组 |
| indexOf | 要查找的项和(可选的)表示查找起点位置的索引。其中, 从数组的开头(位置 0)开始向后查找。没找到返回-1. 返回查找项的索引值 |
| lastIndexOf | 从数组的末尾开始向前查找。返回查找项的索引值(索引值永远是正序的索引值),没找到返回-1 |
| every | 判断数组中每一项都是否满足条件,只有所有项都满足条件,才会返回true。 |
| some | 判断数组中是否存在满足条件的项,只要有一项满足条件,就会返回true |
| filter | 过滤功能,数组中的每一项运行给定函数,返回满足过滤条件组成的数组 |
| reduce | 归并,从数组的第一项开始,逐个遍历到最后,迭代数组的所有项,然后构建一个最终返回的值 |
| reduceRight | (与reduce类似)从数组的最后一项开始,向前逐个遍历到第一位,迭代数组的所有项,然后构建一个最终返回的值。 |
concat 合并
该方法可以把两个数组里的元素拼接成一个新的数组
返回值: 返回拼接后的新数组
let arr1 = [1,2,3];
let arr2 = [4,5,6];
let arr = arr1.concat(arr2);//arr = [1,2,3,4,5,6];
arr1.push(arr2);//arr1 = [1,2,3,[4,5,6]];
join
该方法可以将数组里的元素,通过指定的分隔符,以字符串的形式连接起来。
返回值:返回一个新的字符串
let arr = [1,2,3,4,5];
let str = arr.join('*');
console.log(str)//1*2*3*4*5
slice 截取
该方法可以从数组中截取指定的字段,返回出来
返回值:返回截取出来的字段,放到新的数组中,
array.slice( begin [,end] );
- begin:用于指定选择开始位置的整数(第一个元素的索引为0)。可以使用负值从数组的末尾进行选择。如果省略,则默认值为“0”,是可选择的。
- end:结束从零开始获取的索引。
let arr = [0,1,2,3,4,5];
let newArr = arr.slice(0,3) //newArr = [ 0, 1, 2 ];
let newArr1 = arr.slice(2) //newArr = [ 2, 3, 4, 5 ]
let newArr2 = arr.slice() //newArr = [ 0, 1, 2, 3, 4, 5 ]
let newArr3 = arr.slice(-2) // newArr = [ 4, 5 ]
forEach
arr.forEach(function(item,index,arr){
//里面的function是一个回调函数,
//item: 数组中的每一项;
//index:item 对应的下标索引值
//arr: 就是调用该方法的数组本身
})
map
映射,该方法使用和forEach大致相同,但是该方法有返回值,返回一个新数组,新数组的长度和原数组长度相等
let arr = [1,32,54,6,543];
let res = arr.map(function(item,index,arr){
return item*2;
})
console.log(arr) //[ 1, 32, 54, 6, 543 ]
console.log(res) //[ 2, 64, 108, 12, 1086 ]
every
判断数组中所有的项是否满足要求,如果全都满足,才返回true,否则返回false
let arr3 = [
{ name: "rj", age: 5, done: false },
{ name: "jy", age: 20, done: true },
{ name: "sx", age: 40, done: true }
];
const everyFlag = arr3.every((item)=>{
return item.done
})
console.log(everyFlag) // false
some
const everyFlag = arr3.some((item)=>{
return item.done
})
console.log(everyFlag) // true
filter
filter方法: 有返回值, 过滤出符合条件的元素
let arr = [1, 2, 3, 4, 5, 6];
let res3 = arr.filter(function(item, index) {
return item % 2 === 0;
});
console.log(res3); //[ 2, 4, 6 ]
reduce
/*
total 必需。初始值, 或者计算结束后的返回值
currentValue:必需。当前元素
currentIndex 可选。当前元素的索引
arr 可选。当前元素所属的数组对象
initialValue 可选。传递给函数的初始值
*/
array.reduce(function (total, currentValue, currentIndex, arr), initialValue)
求和计算
let arr = [1, 2, 3, 4, 5, 6];
let sum = arr.reduce((pre,next)=>{
return pre+next
})
console.log(sum)
扁平化数组(拼接数组)
let arr = [[1,2,3],[4,5],[6]] ;
let flatarr = arr.reduce((pre,next)=>{
return pre.concat(next)
})
console.log(flatarr) //[ 1, 2, 3, 4, 5, 6 ]
计算数组中每个元素出现的次数
var names = ['Alice', 'Bob', 'judy', 'judy', 'Alice'];
var countedNames = names.reduce(function (allNames, name) {
// console.log(allNames, '| ' + name);
if (name in allNames) {
allNames[name]++;
} else {
allNames[name] = 1;
}
return allNames;
}, {});
console.log(countedNames) //{ Alice: 2, Bob: 1, judy: 2 }
2.3 ES6新增的方法
使用方法可参照阮一峰ES6
| 方法名 | 功能 | 新增 |
|---|---|---|
| from | 将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象 | |
| of | 根据传入的参数创建一个新数组 | |
| copyWithin | 在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组会修改原数组 | |
| find | 用于找出第一个符合条件的数组成员 | |
| findIndex | 返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1 | |
| findLast | 从数组的最后一个成员开始,依次向前检查,其他都保持不变 | |
| findLastIndex | 从数组的最后一个成员开始,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1 | |
| entries | 返回包含数组所有键值对的@@iterator | |
| keys | 返回包含数组所有索引的@@iterator | |
| values | 返回包含数组所有值的@@iterator | |
| fill | 用静态值填充数组 | |
| includes | 方法返回一个布尔值,表示某个数组是否包含给定的值 | |
| flat | 将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响 | |
| flatMap | 对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法 | |
| at | 接受一个整数作为参数,返回对应位置的成员,并支持负索引 | ES2022 |
三、数组排序算法
| 名称 | 最优 | 平均 | 最坏 | 内存 | 稳定 | 备注 |
|---|---|---|---|---|---|---|
| 冒泡排序 | n | n^2 | n^2 | 1 | Yes | |
| 插入排序 | n | n^2 | n^2 | 1 | Yes | |
| 选择排序 | n^2 | n^2 | n^2 | 1 | No | |
| 堆排序 | n log(n) | n log(n) | n log(n) | 1 | No | |
| 归并排序 | n log(n) | n log(n) | n log(n) | n | Yes | |
| 快速排序 | n log(n) | n log(n) | n^2 | log(n) | No | 在 in-place 版本下,内存复杂度通常是 O(log(n)) |
| 希尔排序 | n log(n) | 取决于差距序列 | n (log(n))^2 | 1 | No | |
| 计数排序 | n + r | n + r | n + r | n + r | Yes | r - 数组里最大的数 |
| 基数排序 | n * k | n * k | n * k | n + k | Yes | k - 最长 key 的升序 |
关于稳定性
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
3.1 冒泡排序
3.1.1 思路
- 冒泡排序只会比较相邻的两个元素
- 若a > b,则交换两个元素
- 对每一对相邻的元素重复上述工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有元素重复1-3步骤,除了最后一个元素
- 直到没有任何一对数字需要比较
3.1.2 实现
function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len - 1; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j+1]) { // 相邻元素两两对比
var temp = arr[j+1]; // 元素交换
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
针对上述排序算法的优化
- 优化外层循环:记录当前循环是否发生变化,如果没有则直接结束
function bubbleSort(arr){
let lan = arr.length
for(let i = 0;i< lan- 1 ;i++){
let flag = 0 //记录每次大循环完毕后是否发生值的交换
for(let j = 0;j<lan-1-i;j++){
if(arr[j]>arr[j+1]){
const temp = arr[j+1]
arr[j+1] = arr[j]
arr[j] = temp
flag++
}
}
if(flag == 0){break;}// 直接跳出循环
}
return arr
}
- 内层优化:记录当前循环中最后一次元素交换的位置,改位置之后的序列都是已经排列好的序列,单独存放一个数组,下次循环无需比较
function bubbleSort2(arr){
let lastIndex=0;
let sortBorder=arr.length-1;
for(let i=0;i<arr.length-1;i++){
let sort=false; //记录每次大循环完毕后是否发生值的交换
for(let j=0;j<sortBorder;j++){ //将sortBorder作为位置边界,作为嵌套循环的循环次数
if(arr[j]>arr[j+1]){ //如果循环中的值前面的值大于后面的值则将前面值与后面值交换
let temp=arr[j]; //这是正序,反之判断为小于则为倒序
arr[j]=arr[j+1];
arr[j+1]=temp;
sort=true;//值发生变化了,记录为true
lastIndex=j;//记录每次小循环发生内容交换的下标
}
}
sortBorder=lastIndex;//获取每次大循环中的所有小循环最终完成后的下标,这就是位置边界
if(!sort){ //sort还是false则证明此循环未发生内容交换,退出整个循环
break;
}
}
return arr
}
3.1.3 复杂度分析
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
- 最好情况下时间复杂度:O(n)
3.2 选择排序
3.2.1 思路
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕
3.2.2 实现
// 选择排序
function selectionSort(arr){
const len = arr.length
var minIndex;
for(let i = 0;i<len;i++){
minIndex = i
for(let j = i+1;j<len;j++){
if(arr[minIndex] > arr[j]){
minIndex = j
}
}
// minIndex为找到的最小元素
// 交换i 与minIndex位置上的元素
swap(arr,minIndex,i)
}
return arr
}
function swap(arr, i, j){
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
3.2.3 复杂度分析
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
3.3 插入排序
3.3.1 思路
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。
- 从第一个元素开始,该元素可以认为已经被排序; 2.取出下一个元素,在已经排序的元素序列中从后向前扫描; 3.如果该元素(已排序)大于新元素,将该元素移到下一位置; 4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置; 5.将新元素插入到该位置后; 6.重复步骤2~5。
3.3.2 实现
function insertionSort(arr) {
var len = arr.length;
var preIndex, current;
for (var i = 1; i < len; i++) {
preIndex = i - 1;
current = arr[i];
while(preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex+1] = arr[preIndex];
preIndex--;
}
arr[preIndex+1] = current;
}
return arr;
}
3.3.3 复杂度分析
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
3.4 归并排序
3.4.1 思路
归并排序是创建在归并操作上的一种有效的排序算法,效率为O(nlogn),该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
分治法
- 分割:递归地把当前序列平均分割成两半
- 集成:在保持元素顺序的同时将上一步得到的子序列集成到一起(归并)
拆分: 首先拆分我们使用数组的 slice 方法对数组进行切割,我们找到中间点,把数组拆成左右两个子数组,在切割的时候要注意左闭右开或者左闭右闭的写法要全程保持一致;其次这种连续拆分,且下一次拆分需要依赖到上一次拆分结果的情况,我们首先想到的是递归。有递归就一定有递归的 return 条件,在这里这个条件是入参的数组长度等于 1,说明已经拆成了最小长度子数组了。
合并: 合并的问题其实就是一个如何合并两个有序数组的问题。我们单独使用一个辅助函数 mergeArray 来进行合并操作。合并操作分为以下几个步骤:
- 定义一个结果数组 res,定义两个待合并数组的下标 i 和 j;
- 比较当前项的大小,将更小项放入 res 中,对应下表加一;
- 某个数组遍历完毕后,将另一个的数组拼接到结果数组中;
3.4.2 实现
//归并
function mergeSort(arr){
// 拆分
var len = arr.length;
if(len < 2) {
return arr;
}
var middle = Math.floor(len / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right)
{
var result = [];
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
while (left.length)
result.push(left.shift());
while (right.length)
result.push(right.shift());
return result;
}
3.4.3 复杂度分析
- 时间复杂度:nlog(n)
- 空间复杂度:O(1)
3.5 快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
3.5.1 思路
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
3.5.2 实现
注意点:基准值的选取:
function quickSort(arr, left, right) {
let len = arr.length,
partitionIndex;
left = typeof left != 'number' ? 0 : left;
right = typeof right != 'number' ? len - 1 : right;
// 递归结束条件:left大于等于right的时候
if (left >= right) {
return;
}
// 得到基准元素的位置
partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex-1);
quickSort(arr, partitionIndex+1, right);
return arr
}
function partition(arr, left, right){
// 设定基准值(pivot)
let pivot = left,index = pivot + 1;
for (var i = index; i <= right; i++) {
if (arr[i] < arr[pivot]) {
swap(arr, i, index);
index++;
}
}
//此时基准值的位置还需要交换
swap(arr, pivot, index - 1);
return index-1;
}
function swap(arr, i, j){
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
3.5.3 复杂度分析
- 时间复杂度:nlog(n)
- 空间复杂度:O(log(n))
归并排序与快速排序的区别:
图片引用自百度图片
从图中我们可以看出
- 归并排序是从下而上的,先切分、后排序。过程可以描述为:切分、切分、切分……排序、排序、排序
- 快速排序与之相反,从上而下,先分区,在处理子元素。过程可以描述为:分区、排序、分区、排序
- 归并排序虽然是稳定的、时间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法
- 归并排序中的操作是将两个数组合并为一(归并操作),快速排序中的操作是交换。
其余排序算法
其余排序算法可参考文档:十大经典排序算法在这边就不做赘述