复杂度
1.时间复杂度计算
1. O(1)
let i=1;
i+=1;
2.O(n)
for(let i=0;i<n;i+=1){
console.log(i)
}
O(1)*O(n)=O(n)
3. O(n)*O(n)=O(n^2)
for(let i=0;i<n;i++){
for(let j=0;j<n;j++){
console.log(i,j)
}
}
4. O(logn)
let i=1;
while(i<n){
console.log(i);
i*=2;
}
空间复杂度计算
算法在运行过程中临时占用存储空间大小的量度
1.O(1)
let i=1;
i+=1;
//只声明了单个变量
2. O(n)
const list=[];
for(let i=0;i<n;i++){
list.push(i)
}
3. O(n^2)
const matrix=[];
for(let i=0;i<n;i++){
martix.push([]);
for(let j=0;j<n;j++){
martix[i].push(j);
}
}
栈
一个后进先出的数据结构,JavaScript中没有栈,但是可以用array实现栈中的功能
栈的应用场景
十进制转二进制、判断字符串的括号是否有效、函数调用堆栈...
leetcode 20 有效的括号
题目描述
1.给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合
左括号必须以正确的顺序闭合。
解题步骤
1.新建一个栈
2.扫描字符串,遇左括号入栈,遇到和栈顶括号(最后进入的括号)类型匹配的右括号就出栈,类型不匹配直接判定为不合法
3.最后栈空了就合法,否则不合法
var isValid = function(s) {
if(s.length%2===1){
return false;
}//如果是奇数,则返回错误,没有必要进入循环了
const stack=[];
for(let i=0;i<s.length;i+=1){
const c=s[i];
if(c==='[' || c==='{' || c==='('){
stack.push(c);//推入栈中
}
else{
const t=stack[stack.length-1];//获取栈顶元素
if(
(t==='(' && c===')')||
(t==='[' && c===']')||
(t==='{' && c==='}') //暴力枚举
){
stack.pop();
}else{
return false;
}
}
}
return stack.length===0;
};
时间复杂度:O(n) 空间复杂度O(n)
前端与栈的结合点
JS中的函数调用堆栈
const fun1=()=>{
fun2();
}
const fun2=()=>{
fun3();
}
const fun3=()=>{}
fun1();
接着往上走,fun2消失,之后fun1
在工作中遇到关于函数的复杂情况,可以打个断点看它们的执行顺序
栈常用操作 push() pop() stack[stack.length-1]//获取栈顶元素
队列
一个先进先出的数据结构,JavaScript中没有队列,可以使用array来实现队列
什么场景用队列
js异步中的任务队列
js是单线程,无法同时处理异步中的并发任务
setTimeout(()=>console.log(1),0);
console.log(2);
//先打印2,后打印1
事件循环与任务队列
计算最近请求次数(leetcode 993)
解题步骤
- 有新请求就入队,3000ms前发出的请求出队
- 队列的长度就是最近请求次数
leetcode-cn.com/problems/nu…
有while循环 时间复杂度:O(n)
新建了一个队列 空间复杂度为O(n)
链表
多个元素组成的列表
元素存储不连续,用next指针连在一起
数组vs链表
数组:增删非首尾元素时往往需要移动元素。
链表:增删非首尾元素,不需要移动元素,只需要更改next的指向即可
JavaScript中没有链表,可以用object来模拟链表
const a={val:'a'};
const b={val:'b'};
const c={val:'c'};
const d={val:'d'};
a.next=b;
b.next=c;
c.next=d;
//遍历链表
let p=a;
while(p){
console.log(p.val);
p=p.next;
}
//插入
const e={val:'e'};
c.next=e;
e.next=d;
//删除
c.next=d;
删除链表中的节点 LeetCode 237
解题思路
- 无法直接获取被删除节点的上个节点
- 将被删除节点转移到上个节点
阶梯步骤 - 将被删除节点的值改为下个节点的值
- 删除下个节点 leetcode-cn.com/problems/de…
反转链表 LeetCode206
反转两个节点:将n+1的next指向n
反转多个节点:双指针遍历链表,重复上述操作
解题步骤
- 双指针一前一后遍历链表
- 反转双指针
var reverseList = function(head) {
let p1=head;
let p2=null;
while(p1){
const tmp=p1.next;
p1.next=p2; //改变指针箭头的方向
p2=p1; //向后走
p1=tmp; //向后走
}
return p2;
};
时间复杂度O(n) 空间复杂度O(1)
两数相加 LeetCode2
解题步骤
- 新建一个空链表
- 遍历被相加的两个链表,模拟相加操作,将个位数追加到新链表上,将十位数留到下一位去相加 复杂度
- 时间复杂度:O(\max(m,n))O(max(m,n)),其中 mm 和 nn 分别为两个链表的长度。我们要遍历两个链表的全部位置,而处理每个位置只需要 O(1) 的时间。
- 空间复杂度:O(1)。注意返回值不计入空间复杂度
var addTwoNumbers = function(l1, l2) {
let addOne=0;//进位
let sum = new ListNode('0') // 创建一个头链表用于保存结果
let head = sum // 保存头链表的位置用于最后的链表返回
while (addOne || l1 || l2) {//在两个链表之中有一个存在的前提下执行下面的逻辑
let val1 = l1?l1.val:0;
let val2 = l2?l2.val:0;//取出左右两边数的最低位,还要判断它是否为空0
let r1 = val1 + val2+addOne;//求和
addOne=r1>=10?1:0;//如果求和大于等于10,那么进位为1,否则为0
sum.next = new ListNode(r1 % 10)//sum的下一个节点
sum = sum.next //sum指向下一个节点
if (l1) l1 = l1.next //l1指向下一个节点,以便计算第二个节点的值
if(l2) l2 = l2.next //l2指向下一个节点,以便计算第二个节点的值
}
return head.next //返回计算结果,之所以用head.next是因为head中保存的第一个节点是刚开始定义的“0”
};
删除链表中的重复元素LeetCode83(简单)
解题步骤
- 遍历链表,如果当前元素和下一个元素的值相等,则删除下一个元素值
- 遍历结束后,返回原来的链表
var deleteDuplicates = function(head) {
let p=head;
while(p && p.next){
if(p.val===p.next.val){
p.next=p.next.next;
}else{
p=p.next;
}
}
return head
};
时间复杂度O(n) 空间复杂度O(1)
环形链表
解题思路
- 用一快一慢两个指针遍历链表,如果指针能够相逢,就返回true
- 遍历结束,没有相逢就返回false
var hasCycle = function(head) {
let p1=head;
let p2=head;
while(p1 && p2 && p2.next){
p1=p1.next;
p2=p2.next.next;
if(p1=p2){
return true;
}
}
return false
};
时间复杂度O(n) 空间复杂度O(1)
前端与链表 JS中的原型链
原型链
- 原型链的本质是链表。
- 原型链上的节点是各种原型对象,比如Function.prototype、Object.prototype......
- 原型链通过__proto___属性连接各种原型对象。 面试常考点
- 如果A能沿着原型链找到B.prototype,则A instanceof B为true
- 如果A对象上没有找到x属性,那么会顺着原型链找x属性 使用链表指针获取JSON的节点值
const json={
a:{b:{c:1}};
d:{e:1};
};
const path=['a','b'];
let p=json;
path.forEach(k=>{
p=p[k];
});
console.log(p);
集合
- 无序且唯一的数据结构
- ES6中有集合,名为set
- 集合的常用操作:去重、判断某元素是否在集合中、求交集
去重、判断元素是否在集合中、求交集
//去重
const arr1=[1,1,2,2];
const arr2=[...new Set(arr1)];
//判断元素是否在集合中
const set=new Set(arr);
const has=new has(1)
//求交集
const set2=new Set([2,3]);
const set3=new Set([...set].filter(item=>set2.has(item))
两个数组的交集 LeetCode349
解题步骤
- 用集合对nums1去重
- 遍历nums1,筛选出nums2也包含的值
var intersection = function(nums1, nums2) {
return [...new Set(nums1)].filter(n=>nums2.includes(n));
};
/把nums1放进数组中,对其进行筛选nums2中是否有n值与nums1中的数值相等
//如果用n=>new Set(nums2).has(n)执行用时和内存消耗比较慢(把nums2变成了一个集合,调用这个集合需要耗时)
时间复杂度O(m*n)
因为filter占一个循环,内部还有一循环嵌套includes
空间复杂度O(m)
前端与集合 使用ES6中的set
使用set对象:new、add、delete、has、size
add()方法
let mySet=new Set();
mySet.add(1);
mySet.add(5);
mySet.add(5);
//set 具有唯一性,无论添加多少次,都只有一个5
mySet.add('some');//set可以添加字符串
let o={a:1,b:2}
mySet.add(o);//set还可以添加对象
mySet.add({a:1,b:2});//这两个对象看似一样,但是在内存中存储的位置是不同的
has()方法 delete()方法
//接上面的代码
const has=mySet.has(5);
### 迭代set:多种迭代方法、set与array互转、求交集/差集
mySet.delete(5);
如何迭代set
for(let [key, value] of mySet.entries()) console.log(key, value);
set与数组的互相转化
const myArr=Array.from(mySet);
const mySet2=new set([1,2,3,4])
求交集和差集
const intersection=new Set([...mySet].filter(x=>mySet2.has(x)));
const difference=new Set([...mySet].filter(x=>!mySet2.has(x)));
字典
- 与集合类似,字典也是一种存储唯一值的数据结构,但他是以键值对的形式来存储
- ES6中有字典,叫Map
字典的常用操作:键值对的增删改查
const m=new Map();
m.set('a','aa');//增
m.delete('a');
m.clear();//删除键的两种方式,clear是全部清空
m.set('a','aaa')//改:把a的值改为aaa
两个数组的交集 LeetCode349
解题步骤
- 新建一个字典,遍历nums1,填充字典
- 遍历nums2,遇到字典里的值就选出,并从字典中删除
var intersection = function(nums1, nums2) {
const map=new Map();
nums1.forEach(n=>{
map.set(n,true)
})
const res=[];
nums2.forEach(n=>{
if(map.get(n)){
res.push(n);
map.delete(n);
}
})
return res;
};
时间复杂度O(m+n)
空间复杂度O(m)
有效的括号 LeetCode20
var isValid = function(s) {
if(s.length%2===1){
return false;
}//如果是奇数,则返回错误,没有必要进入循环了
const stack=[];
const map=new Map();
map.set('{','}');
map.set('[',']');
map.set('(',')');
for(let i=0;i<s.length;i+=1){
const c=s[i];
if(map.has(c)){
stack.push(c);//推入栈中
}
else{
const t=stack[stack.length-1];//获取栈顶元素
if(map.get(t)===c){
stack.pop();
}else{
return false;
}
}
}
return stack.length===0;
};
时间复杂度o(n) 空间复杂度o(n)
两数之和 LeetCode1
解题思路
- 把nums想象成相亲者
- 把target想象成匹配条件
- 用字典建立一个婚姻介绍所,存储相亲者的数字和下标 解题步骤
- 新建一个字典作为婚姻介绍所
- nums的值,逐个来介绍所找对象,没有合适的就先登记着,有合适的就牵手成功
var twoSum = function(nums, target) {
const map=new Map();
for(let i=0;i<nums.length;i++){
const n=nums[i];
const n2=target-n;
if(map.has(n2)){
return [map.get(n2),i];
}else{
map.set(n,i)
}
}
};
时间复杂度o(n)
空间复杂度o(n)
无重复字符的最长子串 LeetCode1
解题思路
- 用双指针维护一个滑动窗口,用来剪切子串
- 不断移动右指针,遇到重复字符,就把左指针移动到重复字符的下一位
- 移动过程中,记录所有窗口的长度,并返回最大值
var lengthOfLongestSubstring = function(s) {
let l = 0;
let res = 0;
const map = new Map();
for(let r = 0; r < s.length; r += 1){
if(map.has(s[r]) && map.get(s[r]) >= 1){
l = map.get(s[r]) + 1;
}
res = Math.max(res, r - l + 1);
map.set(s[r], r);
}
return res;
};
上面这个解法不知道为什么一直报错,呜呜呜,,,
下面是官网给出的其他解法:
var lengthOfLongestSubstring = function(s) {
// 哈希集合,记录每个字符是否出现过
const occ = new Set();
const n = s.length;
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
let r = -1, ans = 0;
for (let i = 0; i < n; ++i) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.delete(s.charAt(i - 1));
}
while (rk + 1 < n && !occ.has(s.charAt(rk + 1))) {
// 不断地移动右指针
occ.add(s.charAt(rk + 1));
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = Math.max(ans, rk - i + 1);
}
return ans;
};
最小覆盖子串 LeetCode76
解题步骤
- 用双指针维护一个滑动窗口
- 移动右指针,找到包含T的子串,移动左指针,尽量减少包含T的子串的长度
var minWindow = function(s, t) {
let l = 0;
let r = 0;
const need = new Map();
for (let c of t) {
need.set(c, need.has(c) ? (need.get(c)+1) : 1);//新建一个字典,用来表示需要的字符以及个数
}
let needType = need.size;
let res = '';
while(r < s.length){
const c = s[r];//获取右指针当前的字符
if(need.has(c)){
need.set(c, need.get(c)-1);//如果右指针有当前字符,则需要的个数减一
if(need.get(c)===0) needType -= 1;//如果t中某个需要的字符在s中没有了,则needType的值减一
}
while(needType===0){
const newRes=s.substring(l,r+1);
if(!res || newRes.length<=res.length) res=newRes;
const c2=s[l];
if(need.has(c2)){
need.set(c2,need.get(c2)+1);
if(need.get(c2)===1) needType+=1;
}
l+=1;
}
r+=1;//移动右指针
}
return res
};
树
一种分层数据的抽象模型
前端工作中常见的树包括:DOM树、级联选择、树形控件...
JS中没有树,但是可以用Object和Array构建树
数的常用操作 深度、广度优先遍历,先中后序优先遍历
深度、广度优先遍历
深度优先遍历:尽可能深的访问数的分支
广度优先遍历:先访问离根节点最近的节点
深度优先遍历算法口诀
- 访问根节点
- 对根节点的children挨个进行深度优先遍历 广度优先遍历算法口诀
- 新建一个队列,并把根节点入队
- 把队头出队并访问
- 把队头的children挨个入队
- 重复第二三步,直到队列为空
二叉树的先中后序遍历
二叉树:树中每个节点最多只能有两个子节点
先序遍历算法口诀
- 访问根节点
- 对根节点的左子树进行先序遍历
- 对根节点的右子树进行先序遍历
const bt = require('./bt');
const precoder = (root) =>{
if(! root){ return; }
console.log(root.val);
precoder(root.left);
precoder(root.right);
}
precoder(bt)
中序遍历算法口诀
- 对根节点的左子树进行先序遍历
- 访问根节点
- 对根节点的右子树进行先序遍历
后序遍历算法口诀 左右后
非递归版先中后序遍历
先序遍历
const precoder = (root) =>{
if(!root){return:}
const stack=[root];
while(stack.length){
const n=stack.pop(); //将根节点从栈里弹出来,下一步访问根节点的值
console.log(n.val);
if(n.right) stack.push(n.right);
if(n.left) stack.push(n.left);
}
};
中序遍历
const inrorder = (root) =>{
if(!root) {return;}
const stack=[];
let p=root; //此时需要用到指针
while(stack.length || p) {
while(p){
stack.push(p);
p=p.left;
}
const n=stack.pop();
console.log(n.val);
p = n.right;
}
}
后序遍历
const postorder = (root) =>{
if(!root){return;}
const outputStack = [];
const stack = [root];
while (stack.length){
const n = stack.pop();
outputStack.push(n);
if(n.left) stack.push(n.left);
if(n.right) stack.push(n.right);
}
while(outputStack.length){
const n = outputStack.pop();
console.log(n.val);
}
};
二叉树的最大深度 LeetCode104
解题思路
- 求最大深度。考虑使用深度优先遍历
- 在深度优先遍历过程中,记录每个节点所在的层级。找出最大的层级即可 解题步骤
- 新建一个变量,记录最大深度
- 深度优先遍历整棵树,并记录每个节点的层级,同时不断刷新最大深度这个变量
- 遍历结束返回最大深度这个变量
var maxDepth = function(root) {
let res=0; //用一个变量来记录最大深度
const dfs = (n,l) => { //深度优先遍历整棵树
if (!n) {return;}
if(!res.left && !res.right){
res = Math.max(res,l);
}//这一步判断是否为叶子节点,如果是的话,就刷新树的深度
dfs(n.left,l+1);
dfs(n.right,l+1);
}
dfs(root,1);
return res;
};
复杂度分析
- 时间复杂度:O(n),其中 n为二叉树节点的个数。每个节点在递归中只被遍历一次。
- 空间复杂度:O(height),其中height 表示二叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。
二叉树的最小深度
解题思路
- 求最小深度,建议用广度优先遍历
- 在遍历中,遇到叶子结点即返回当前叶子节点的层级 解题步骤
- 广度优先遍历整棵树,记录每个节点的层级
- 遇见叶子结点,返回节点层级,停止遍历
var minDepth = function(root) {
if(!root){ return 0; }
const q=[[root,1]];//广度优先遍历需要用到队列
while(q.length){
const [n,l]=q.shift();
if(!n.left && !n.right){
return l;
}//判断叶子结点
if(n.left) q.push([n.left,l+1]);
if(n.right) q.push([n.right,l+1]);//把当前节点的孩子节点推入到队列中
}
};
- 时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
- 空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。
二叉树的层级遍历
解题步骤
- 广度优先遍历二叉树
- 遍历过程中,记录每个节点的层级,并将其添加到不同的数组中
var levelOrder = function(root) {
if(!root){
return []
};
const q=[[root,0]];
const res=[];
while(q.length){
const [n,level]=q.shift();
if(!res[level]){
res.push([n.val]);//数组为空,给个[]
}else{
res[level].push(n.val);//把值推入到该数组中
}
if(n.left) q.push([n.left,level+1]);
if(n.right) q.push([n.right,level+1]);
}
return res;
};
记树上所有节点的个数为 n
- 时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n)
- 空间复杂度:队列中元素的个数不超过 n 个,故渐进空间复杂度为 O(n)
中序遍历LeetCode:94
简单的递归算法
var inorderTraversal = function(root) {
const res=[];
const rec = (n) =>{
if(!n){return;}
rec(n.left);
res.push(n.val);
rec(n.right);
}
rec(root);
return res;
};
较为复杂的迭代算法
var inorderTraversal = function(root) {
const res=[];
const stack=[];
let p=root;
while(stack.length || p){
while(p){
stack.push(p);//把指针所指的节点推到堆栈中
p=p.left;//遍历所有的子节点
}
const n=stack.pop();//访问节点,其实就是一个出栈的过程
res.push(n.val);
p=n.right;//p指针指向它的右节点
}
return res;
};
路径总和 LeetCode112
解题步骤
- 深度优先遍历二叉树,在叶子结点处,判断当前路径的节点值的和是否等于目标值。是就返回true
- 遍历结束,如果没有匹配,就返回false
var hasPathSum = function(root, targetSum) {
if(!root) return false;
let res=false;
const dfs = (n,s) =>{
if(!n.left && !n.right && s===targetSum){
res = true;
}
if(n.left) dfs(n.left,s + n.left.val);
if(n.right) dfs(n.right,s + n.right.val);
}
dfs(root,root.val);
return res;
};
- 时间复杂度 O(n)
- 空间复杂度 O(n) 递归调用一个函数调用堆栈
遍历JSON的所有节点值
const json = {
a: { b: { c: 1 } },
d: [1, 2],
};
const dfs = (n, path) => {
console.log(n, path);
Object.keys(n).forEach(k => {
dfs(n[k], path.concat(k));
});
};
dfs(json, []);
图
图是网络结构的抽象模型,是一组由边连接的节点
JS中没有图,但是可以用object和array来构建图
图的深度优先、广度优先遍历
深度优先遍历算法
- 访问根节点
- 对根节点没有访问过的相邻节点挨个进行深度优先遍历
const graph = {
0: [1, 2],
1: [2],
2: [0, 3],
3: [3]
}; //邻接表表示法
const visited = new Set();
const dfs = (n) => {
console.log(n);
visited.add(n);
graph[n].forEach(c => {
if(!visited.has(c)){
dfs(c);
}
});
};
dfs(2);
广度优先遍历
- 新建一个队列,把根节点入队
- 把队头出队并访问
- 把队头没访问过的相邻节点入队
- 重复第二三步,直到队列为空
const visited = new Set();
const q = [2];
visited.add(2);
while(q.length){
const n = q.shift();
//shift删除原数组第一项,并返回删除元素的值
graph[n].forEach(c=>{
if(!visited){
q.push(c);
//push接收参数,把他们添加到数组末尾
visited.add(n);
}
})
}
有效的数字LeetCode65(难
太平洋大西洋水流问题 leetcode417
用的是图的深度优先遍历
解题步骤
- 新建两个矩阵,分别记录能流到两个大洋的坐标
- 从海岸线,多管齐下,同时深度优先遍历图,过程中填充上述矩阵
- 遍历两个矩阵,找到能流到两个大洋的坐标
leetcode-cn.com/problems/pa…
时间复杂度和空间复杂度都是m*n
克隆图LeetCode133
堆
堆是一种特殊的完全二叉树
- 所有的节点都大于等于(最大堆)或者小于等于(最小堆)他的子节点
- JS中一般用数组表示堆
- 左侧子节点的位置是
2*index+1 - 右侧子节点的位置是
2*index+2 - 父节点位置是
(index-1)*2
js实现最小堆类
- 步骤
- 在类里,声明一个数组,用来装元素
- 主要方法:插入、删除堆项、获取堆顶,获取堆大小
- 插入
- 将值插入堆的底部,即数组的尾部。
- 然后上移:将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值。
- 大小为k的堆中插入元素的时间复杂度为o(logk)
class minHeap{
constructor(){
this.heap = [];
}
//获取父节点
getParentIndex(i){
return (i-1) >> 1;//取商的一个方法
}
swap(i1,i2){
const temp = this.heap[i1];//声明临时变量,来存储i1的值
this.heap[i1] = this.heap[i2];
this.heap[i2] = temp;
}
shiftUp(index){
if(index == 0){ return;}//上移到堆顶,就不要再上移了
const ParentIndex =this.getParentIndex(index);//获取父节点
if(this.heap[ParentIndex] > this.heap[index]){
this.swap(ParentIndex,index); //如果父节点的值大于子节点的值,进行交换
this.shiftUp()
}
}
insert(value){
this.heap.push(value); //把它放在数组的最后一位
this.shiftUp(this.heap.length-1);//上移操作
}
}
const h =new minHeap();
h.insert(3);//检查一下
删除堆顶
- 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)。
- 然后下移:将新堆顶和它的子节点进行交换,直到子节点大于等于这个新堆顶。
- 大小为k 的堆中删除堆顶的时间复杂度为O(logk)。
shiftdown(index){
const leftindex = this.getLeftIndex(index);
const rightindex = this.getRightIndex(index);
if(this.heap[leftindex] < this.heap[index]){
this.swap(leftindex,index);
this.shiftdown(leftindex);
}
if(this.heap[rightindex] < this.heap[index]){
this.swap(rightindex,index);
this.shiftdown(rightindex);
}
}
pop(){
this.heap[0] = this.heap.pop();
this.shiftdown()
}
数组中的第k个最大元素 LeetCode215
前k个高频元素 LeetCode347
var topKFrequent = function(nums, k) {
const map = new Map();
nums.forEach(n=>{
map.set(n,map.has(n) ? map.get(n) + 1 : 1);//建立映射关系
})
const list = Array.from(map).sort((a,b)=> b[1] - a[1]);//sort方法的应用
return list.slice(0,k).map(n=>n[0]);//本来输出的是频率,但加上map之后就是这个元素值
};
但是题目说时间复杂度要优于O(nlogn),所以我们现在就要用到堆 leetcode-cn.com/problems/to…
进阶算法之搜索排序
排序 把某个乱序的数组变成升序或者降序的数组 sort()方法 搜索indexOf()方法