数据结构-----数组

127 阅读13分钟

一、什么是数组

1.1 数组

数组 数据结构是由相同类型的集合所组成的数据结构,分配一块连续的内存来存储。利用元素的索引(index)可以计算出该元素对应的存储地址。

由上述定义中我们可以提取两个重点内容

  1. 相同数据类型的集合(同一类型元素所需存储空间大小一致,所以我们可以很方便的利用元素的索引来计算出元素所在的位置)
  2. 分配一块连续的内存来存储

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

三、数组排序算法

十大经典排序算法

名称最优平均最坏内存稳定备注
冒泡排序nn^2n^21Yes
插入排序nn^2n^21Yes
选择排序n^2n^2n^21No
堆排序n log(n)n log(n)n log(n)1No
归并排序n log(n)n log(n)n log(n)nYes
快速排序n log(n)n log(n)n^2log(n)No在 in-place 版本下,内存复杂度通常是 O(log(n))
希尔排序n log(n)取决于差距序列n (log(n))^21No
计数排序n + rn + rn + rn + rYesr - 数组里最大的数
基数排序n * kn * kn * kn + kYesk - 最长 key 的升序

关于稳定性

稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。

不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

3.1 冒泡排序

3.1.1 思路

  1. 冒泡排序只会比较相邻的两个元素
  2. 若a > b,则交换两个元素
  3. 对每一对相邻的元素重复上述工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  4. 针对所有元素重复1-3步骤,除了最后一个元素
  5. 直到没有任何一对数字需要比较

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²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕

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)的算法描述是一种简单直观的排序算法。

  1. 从第一个元素开始,该元素可以认为已经被排序; 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 思路

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(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))

归并排序与快速排序的区别:

image.png 图片引用自百度图片 从图中我们可以看出

  1. 归并排序是从下而上的,先切分、后排序。过程可以描述为:切分、切分、切分……排序、排序、排序
  2. 快速排序与之相反,从上而下,先分区,在处理子元素。过程可以描述为:分区、排序、分区、排序
  3. 归并排序虽然是稳定的、时间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法
  4. 归并排序中的操作是将两个数组合并为一(归并操作),快速排序中的操作是交换。

其余排序算法

其余排序算法可参考文档:十大经典排序算法在这边就不做赘述