回顾一遍算法。

60 阅读17分钟

数据结构:计算机存储或者组织数据的方式

算法: 解决问题的方式

时间复杂度:

1. 是什么?

    执行当前算法所"花费的时间"

2. 干什么?

    写代码的过程中,就可以大概知道代码运行的快与慢

3. 表示

    大O表示法: 《解析数论》

    常用时间:O(1) O(n) O(n^2) O(logn)

O(n),

也叫线性时间,这样的算法包括简单查找。

O(log n),

也叫对数时间,这样的算法包括二分查找。

O(n * log n),

这样的算法包括第4章将介绍的快速排序——一种速度较快的排序算法。

O(n2 ),

这样的算法包括第2章将介绍的选择排序——一种速度较慢的排序算法。

O(n!),

这样的算法包括接下来将介绍的旅行商问题的解决方案——一种非常慢的算法。

空间复杂度:

 1. 是什么?

    执行当前算法需要占用多少内存空间

2. 表示法

    常用空间:O(1) O(n) O(n^2) O(logn)

栈、队列

都是进栈、出栈

特点:后进先出

入:push [1,2,3,4]、

出:pop [1,2,3] ==> 4

力扣20有效的括号

var isValid = function (s) {
        var stack = [];
        for (let i = 0; i < s.length; i++) {
                const start = s[i];
                if (s[i] == '(' || s[i] == '{' || s[i] == '[') {
                        stack.push(s[i]);
                } else {
                        const end = stack[stack.length - 1];
                        if (start == ")" && end == '(' ||
                                start == "}" && end == '{' ||
                                start == "]" && end == '['
                        ) {
                                stack.pop()
                        } else {
                                return false
                        }
                }
        }
        return stack.length == 0
};
console.log(isValid('()[]{}'));
console.log(isValid('()[]{]'));
console.log(isValid('(){]{]'));

力扣1047删除字符串中的所有相邻重复项

var removeDuplicates = function(s) {
    let stack = [];

    for( v of s ){
        let prev = stack.pop();
        if(  prev != v ){
            stack.push( prev );
            stack.push( v );
        }

    }
    return stack.join('');

};
console.log( removeDuplicates('abbaca') );

力扣71 简化路径

var simplifyPath = function(path) {
    let stack = [];
    let str = '';
    let arr = path.split('/');
    arr.forEach( val=>{
        if(  val && val == '..' ){
            stack.pop();
        }else if( val && val != '.'){
            stack.push( val );
        }
    })
    arr.length ? str = '/' + stack.join('/') : str = '/';
    return str; 
};
console.log( simplifyPath('/home//foo/') );

队列

队列 : 先进先出 let arr = [];

arr.push(1);
arr.push(2);

console.log( arr );

arr.shift();

console.log( arr );

任务队列

了解事件机制,方便后续的题目讲解

同步 、 异步(定时器、事件、请求...)

异步分为:宏任务( 定时器 )和 微任务(promise.then)

JS执行流程

  1. 主线程读取JS代码,此时同步环境,形容对应的堆和执行栈

  2. 主线程遇到异步任务,会推给异步线程进行处理

  3. 异步进行处理完毕,将对应的异步任务推入任务队列

  4. 主线程查询任务队列,执行微任务,将其按照顺序执行,全部执行完毕。

  5. 主线程查询任务队列,执行宏任务,取得第一个宏任务,执行完毕。

  6. 重复以上4,5步骤。

Promise.resolve('3333').then(res=>{
	console.log( res );

	setTimeout(()=>{
		console.log('Promise setTimeout')
	},0)
})

setTimeout(()=>{
	console.log( 111 );
	Promise.resolve('setTimeout Promise').then(res=>{
		console.log( res );
	})

},0)


console.log( 222 );

力扣933最近的请求次数

链表、数组

多个元素存储组成的

·

一、什么是链表

  1. 多个元素存储的列表

  2. 链表中的元素在内存中不是顺序存储的,而是通过"next"指针联系在一起的。

    ***js中的原型链 原理就是 链表结构

    解释next指针:如下
    {
        val:'a',
        next:{
            val:'b',
            next:{
                val:'c',
                next:{
                    val:'d',
                    next:null
                }
            }
        }
    }
    

    对链表的操作(插入、删除)都是通过指针来进行的:如下

    //遍历链表
    let obj = a;
    while( obj && obj.key ){
        console.log( obj.key );
        obj = obj.next;
    }
    
    //链表中插入某个元素
    let m = {key:'mmmmm'};
    c.next = m;
    m.next = d;
    console.log( a );
    
    //删除操作
    c.next = d;
    
    

二、链表和数组的区别

1. 数组:有序存储的,在中间某个位置删除或者添加某个元素,其他元素要跟着动。

2. 链表中的元素在内存中不是顺序存储的,而是通过"next"指针联系在一起的。

三、链表分类

单向链表 (1个指针)

双向链表  (2个指针)

环形链表

instanceof原理

const instanceofs = (target,obj)=>{
	let p = target;
	while( p ){
		if( p == obj.prototype ){
			return true;
		}

		p = p.__proto__;
	}
	return false;
}

console.log( instanceofs( [1,2,3] , Object ) )

力扣237 删除链表中的节点

/** 链表中的元素是通过指针next指向下一个元素,一级一级找,只到null.

 * @param {ListNode} node 当前节点
 * @return {ListNode}
 */
 var deleteNode = function(node) {
    node.val = node.next.val;//当前节点的值等于下一个节点
    node.next = node.next.next;//当前节点的指针指向下下个节点
};

力扣237 删除链表中的重复元素

/** 链表中的元素是通过指针next指向下一个元素,一级一级找,只到null.

 * @param {ListNode} head 头节点
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    if(!head) return head;//从头节点开始找
	let cur = head;
	while(cur.next){
        //当前节点的值与下一个节点的值相等,表示节点重复。  重复则用下个节点的覆盖前一个节点
		if(cur.val == cur.next.val){ 
			cur.next = cur.next.next
		}else{
			cur = cur.next
		}
	}
    return head;
}

力扣206 反转链表

 /** 思路:将链表的 头尾节点互换
    * @param {ListNode} head 链表头节点
    * @return {ListNode}
    */
var reverseList = function (head) {
    let prev = null; //头节点
    let cur = head;//尾节点
    while (cur) {
        const next = cur.next; //将当前的节点指针存储
        cur.next = prev;//把尾节点赋值到头节点
        prev = cur;
        cur = next;
    }
    return prev;
};

环形链表

指针的指向永远不为空

var hasCycle = function(head){
    // f,s是2个指针
    let f= head,s=head;
    // f不为null,f.next不为null,就一直循环
    while(f!=null && f.next!=null){
        s = s.s.next;//当前的指针
        f=  f.next.next;//下一个指针
        if(s==f)return true
    }
    return false;
}

字典

前提知识:对象的键是字符串,若键重名,则后面的覆盖前面的。

字典 : 键值对存储的,类似于js的对象(键[key]都是字符串类型或者会转换成字符串类型)

核心:字典 是通过map来表示的,map的键不会转换类型

    var obj = {
        a:1,
        a:2
    }
    console.log(a) //2 键名一样,后面的覆盖前面的
var a = {}
var b = {
    key:'a'
}	
var c = {
    key:'c'
}
a[b] = '123';   
a[c] = '456';	
//键名相同,后一个会覆盖前一个 ,通过对象obj 理解
console.log( a[b] ); //456 

哈希表 【散列表】

理解:哈希表 类似 电话簿,其中每个姓名都有对应的电话号码。查找张三的电话号码,为此只需向散列表传入相应的键张三。

-- 无需查找,只需根据

js中没有哈希表哈希表字典一种实现。

字典与哈希表区别

区别一:如果找key对应的value需要遍历key,那么想要省去遍历的过程,用哈希表来表示。

区别二:排列顺序

  • 字典是根据添加的顺序进行排列的

  • 哈希表是随机进行排列的

力扣01两数之和

力扣217存在重复元素

力扣349两个数组的交集

力扣1237 独一无二的出现次数

面试:统计一个字符串中出现次数最多的字符

力扣03无重复字符的最长子串

深度优先遍历 与 广度优先遍历

eg: 如何通过深度优先遍历 与 广度优先遍历实现下面树结构如下:

 const tree = {
        val:'a',
        children:[
            {
                val:'b',
                children:[
                    {val:'d',children:[]},
                    {val:'e',children:[]},
                ]
            },
            {
                val:'c',
                children:[
                    {val:'f',children:[]},
                    {val:'g',children:[]}
                ]
            }
        ]
    }

一、深度优先搜索(遍历)

概念:

出发,尽可能的搜索树的节点

技巧:(递归实现)

  1. 访问根节点

  2. 对根节点的children挨个进行深度优先搜索

代码

    //深度优先遍历
    const fun1 = (root)=>{
        console.log( root.val );
        root.children.forEach( fun1 );
    }
    fun1(tree);

二、广度优先搜索(遍历)

概念

出发,优先访问离根节点最近的节点

技巧:(出栈进栈实现)

    1. 新建一个队列,把根节点入队
    2. 把队头出队 
    3. 把队头的children挨个入队
    4. 重复2和3步,直到队列为空为止
    

代码

  //广度优先遍历
    const fun2 = (root)=>{
        const arr = [root];
        while( arr.length > 0 ){
            const o = arr.shift();
            console.log( o.val );
            o.children.forEach(item=>{
                arr.push( item );
            })
        }
    }
    fun2(tree);

力扣104 翻转二叉树

一种分层数据的抽象模型

简单来说:分层级关系的 。 类似公司组织结构 ,虚拟dom就是树型结构

二叉树 (根节点下永远只有左右2个节点)

eg: 如果通过二叉树遍历下面树的节点?

// 二叉树结构
const tree = {
        val:'1',
        left:{
            val:'2',
            left:{val:'4',left:null,right:null},
            right:{val:'5',left:null,right:null}
        },
        right:{
            val:'3',
            left:{val:'6',left:null,right:null},
            right:{val:'7',left:null,right:null}
        }
    }

前序遍历 即(先序遍历)

核心:根左右

1、递归实现

    
    //递归方式
    var preorderTraversal = function(root) {
        let arr = [];
        var fun = ( node )=>{
            if( node ){
                //先根节点
                arr.push( node.val );
                //遍历左子树
                fun( node.left );
                //遍历右子树
                fun( node.right );
            }
        }
        fun( root );
        return arr;
    };
    console.log( preorderTraversal(tree) );

2、非递归版的形式

    //非递归版的形式
    var preorderTraversal = function(root) {
        if( !root ) return [];
        let arr = [];
        //根节点入栈
        let stack = [root];
        while( stack.length ){
            //出栈
            let o = stack.pop();
            arr.push( o.val );

            o.right && stack.push( o.right );
            o.left && stack.push( o.left );
        }
        return arr;
    }
    console.log( preorderTraversal(tree) );

中序遍历

核心:左根右 ,先遍历完左节点,在遍历跟节点。

1.递归实现

    //递归
     var inorderTraversal = function(root) {
        const arr = [];
        const fun = ( root )=>{
                if( !root ) return;
                fun( root.left );
                arr.push( root.val );
                fun( root.right );

        }
        fun( root );
        return arr;
     };

    console.log( inorderTraversal(tree) );

2、非递归实现

//非递归
<!-- var inorderTraversal = function(root) {

	const arr = [];
	const stack = [];
	let o = root;
	while( stack.length || o ){
		while( o ){
			stack.push( o );
			o = o.left;
		}
		const n = stack.pop();
		arr.push( n.val );
		o = n.right;
	}
	return arr;

}
console.log( inorderTraversal(tree) ); -->

后序遍历

核心:左右根 。先遍历完所有左节点,在遍历完右节点

1.递归实现

//递归
 var postorderTraversal = function(root) {
 	const arr = [];
 	const fun = ( node )=>{
 		if( node ){
 			fun( node.left );
 			fun( node.right );
 			arr.push(  node.val );
 		}
 	}
 	fun( root );
 	return arr;
 };

 console.log( postorderTraversal(tree) );

2.非递归实现

//非递归
var postorderTraversal = function(root) {
	if( !root ) return [];
	let arr = [];
	let stack = [root];
	while( stack.length ){
		const o = stack.pop();
		arr.unshift( o.val );
		o.left && stack.push( o.left );
		o.right && stack.push( o.right );
	}
	return arr;
}
console.log( postorderTraversal(tree) );

排序(排序啦)

1.冒泡排序

对存放元素的数列,按从前往后的方向进行多次扫描,每次扫描称为一趟。 当发现相邻两个数据顺序错误,将这2个元素进行互换。 相当于比较相邻2个“气泡”的轻重,重的下沉,轻的上浮。 若从小到大排序,较小的数据就会逐个向前移动,好像气泡向上漂浮一样。

个人理解:从前往后依次扫描排序的数列,每次比较相邻2个元素,若是顺序错误,就交换他们的位置

运行时间O(n*n) = O(n²)

特点:

eg: 对一组数实现从小到大排序

function arrSort( arr ){
	for(let i=0;i<arr.length-1;i++){
		for(let j=0;j<arr.length-1-i;j++){
                //核心:相邻元素两两对比,若是顺序错误,元素交换
			if( arr[j] > arr[j+1]){ 
				let temp = arr[j];   
				arr[j] =  arr[j + 1];
				arr[j + 1] = temp;
                                console.log( '第'+(i+1)+'轮,第'+(j+1)+'次交换',arr );
			}
		}
	}
	return arr;
}
let arr = [29,10,14,37,14];
console.log( arrSort( arr ) );

冒泡排序优化一

立一个 标志位flag,记录一趟序列遍历中元素是否发生交换,没有交换表示该序列已经有序,可以提前结束了。

function arrSort1( arr ){
	for(let i=0;i<arr.length-1;i++){
		let isSwap = false;//初始化都是未交换 false:未交换 true:表示交换
		for(let j=0;j<arr.length-1-i;j++){
			// 交换 设立flag
			if( arr[j] > arr[j+1]){
				isSwap = true; 
				let temp = arr[j];
				arr[j] =  arr[j + 1];
				arr[j + 1] = temp;
				console.log( '---第'+(i+1)+'轮,第'+(j+1)+'次交换',arr );
			}
		}

		if(!isSwap){ //未交换退出循环
			break
		}
		
	}
	return arr;
}
console.log( arrSort1( arr1 ) );

2.选择排序(又叫简单选择排序)

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

个人理解:每次选出剩余元素中最大的或者最小放在已排序序列的末尾。

特点

  • 需要检查的元素数越来越少

  • 排序中每一轮会把最大或最小的数移到最前,所以相互比较的次数每一轮都会比前一轮少1次。 依次n, n-1,n-2,n-3,...1;

  • 选择排序法把N个数通过N-1轮排序。

运行时间O(n*n) = O(n²)

eg:歌曲排序,播放次数越多的靠前。 每次找到播放次数最多的放在表中。找第一个需n次,找第二个需n-1次,以此直到找到最后一个

function sortMin( arr ){
	let indexMin = 0;  //最小值的索引
	for( let i=0;i<arr.length-1;i++){
		indexMin = i; //假定当前索引未最小值的索引
                 //核心:寻找最小的数,将最小数的索引保存
		for( let j=i+1;j<arr.length;j++){ 
			if( arr[j] < arr[indexMin] ){
				indexMin = j; 
			}
		}
		let temp = arr[i];
		arr[i] = arr[indexMin];
		arr[indexMin] = temp;
	}
	return arr;
}
let arr = [29,10,14,37,14];
console.log( sortMin(arr) );

3.插入排序

构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 个人理解:将元素分为已排序的序列 和 未排序的序列2部分。 从当前元素向前比较,扫描已排序的序列,插入的元素与已排序的某个元素相等则插入。

已排序的元素大于(小于)新元素,将元素移动下一个位置。

特点

  1. 从第一个元素开始,该元素可以被认为已经被排序

运行时间O(n*n) = O(n²)

//从小到大排序
function insertionSort(arr) {
    var len = arr.length;
    var preIndex=0;//已排序的元素索引 
	var current;//当前要比较的元素cur 
    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--;
        }
		// 前一个元素(array[j - 1])和后一个元素(array[j])是相同的
        // 在下一轮时,当array[j - 1]小于或等于cur 时,将cur 插入array[j](即上一轮的array[j - 1])
        arr[preIndex+1] = current;
    }
    return arr;
}
let arr1 = [5,6,4,2,1,7];

console.log(  insertSort(arr1)  );

希尔排序(递减增量排序)

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本

理解:将数量按d间隔分组,距离为d的元素放在同一个子序列,在每一个子序列中分别实行直接插入排序。然后缩小间隔d,重复。

步骤: 1.间隔分组 (距离为d的元素放在同一个子序列) 2.组内排序 3.重新设置间隔 4.插入排序

image.png

function shellSort(arr) {
    var len = arr.length;
    let gap = Math.floor(len / 2);//初始化增量
    let temp;
    console.log(len, gap);
    // 每一趟后,需要重新设置间隔
    for (gap = gap; gap > 0; gap = Math.floor(gap / 2)) {
        console.log('增量', gap);
        // 组内,每一趟采用插入排序
        for (var i = gap; i < len; i++) {
            temp = arr[i];
            // 比较大小,交换位置
            for (j = i; j >= gap && temp < arr[j - gap]; j -= gap) {
                arr[j] = arr[j - gap]

            }
            arr[j] = temp;
        }
    }
    return arr;
}

let arr = [1, 7, 4, 7, 8, 9, 3, 21, 17, 6, 5]
console.log(shellSort(arr));

4.归并排序

利用归并的思想实现的排序算法,该算法采用经典的 分治法。分治法分为两个阶段。

分阶段:自上而下的递归 。 分解成多个子序列

治阶段:自下而上的迭代 。 合并2个有序子序列

image.png

// 归并 - 拆分+合并
let arr = [8, 4, 5, 7, 1, 3, 6, 2];
function mergeSort1(arr) {
        // 分阶段:拆分成多个子序列
        if (arr.length < 2) return arr;
        let mid = Math.floor(arr.length / 2);
        let left =  arr.slice(0, mid);
        let right = arr.slice(mid);
        console.log('分-',left, right);

        // 合阶段: 合并2个有序的子序列
        let merge = function (leftArr, rightArr) {
                console.log(leftArr, rightArr);
                let resultArr = [];
                while (leftArr.length && rightArr.length) {
                        // shift() 删除数组的第一项元素,返回被删除的元素, 修改原数组
                        // 若左数组第一个小于等于右边的第一个值,则添加左数组。 
                        // resultArr.push(leftArr[0] <= rightArr[0] ? leftArr.shift() : rightArr.shift())
                        if(leftArr[0] <= rightArr[0] ){
                                resultArr.push(leftArr.shift())
                        }else{
                                resultArr.push(rightArr.shift())
                        }

                }
                // 最后合并
                return resultArr.concat(leftArr).concat(rightArr);
        }

        return merge(
                mergeSort1(left),
                mergeSort1(right)
        );
}

console.log(mergeSort1(arr));

5.快速排序(优于归并排序)

快速排序使用分治法来把一个序列 分为 两个序列个人理解: 从数列中选一个基准值做参照,将小的放在基准值前面,将大放在基准值右边。 称作分区操作

注意:

必须设基准值, 可以是数组中任何一个元素 【注设定了基准值后要从原数组删除】

eg: 对数组从小到大排序,以任意一个元素a为基准值,将小于a的所有数放在一起,将大于a的所有数放在一起。最后通过递归调用完成排序

let arr = [29,10,14,37,4,6,12,8,7];
function quickSort( arr ){

	if( arr.length <=1 ) return arr;
	let mid = Math.floor(  arr.length/2  );
	let pivot = arr.splice(mid,1)[0]; //设置基准值。
	console.log('基准值',pivot);
	let left =[]; // 分区
	let right = [];

	for( let i=0;i<arr.length;i++){
		if( arr[i] <pivot ){
			left.push( arr[i] );
		}else{
			right.push( arr[i] );
		}
	}
	console.log('分区',left,right);
	
	// 递归调用
	// return quickSort(left).concat([pivot],quickSort(right));
	return [...quickSort(left),pivot,...quickSort(right)]
}

console.log( quickSort(arr) );

7.二分查找(折半查找)

是一种在有序数组中查找特定元素的搜索算法。

思想:通过与数组中间元素比较来逐步缩小搜索范围,每次减少一半 大于目标元素,则将指针移动

注意: 必须有序

image.png


let arr = [1,5,10,14,20,21,30,36,40]; 
let target = 30;
// 3个指针分别指向最左边和最右边和中间元素的位置 。
function search( arr , target ){
	let start = 0; //起始索引
	let end = arr.length-1;//最后索引
	let mid = 0; //中间索引 
	while(  start <= end  ){
		mid = Math.floor( (start+end)/2 ) ;
		let guess = arr[mid];//中间值
		//如果中间 == 目标值
		if( guess== target ){
			console.log('猜对了','索引是'+mid);
			break;
		}
		// 目标值大于猜想值,左指针向右移动
		if( target > guess ){
			start = mid + 1;
			console.log('猜小了');
		}
		// 目标值小于猜想值,右指针向左移动
		if( target <  guess){
			end = mid - 1;
			console.log('猜大了');
		}
		console.log('中间索引,',mid,'假设猜想值:'+mid)
	}

}
console.log( search( arr , target) );

堆排序

一、堆是什么?

堆是一种特殊的树,只要满足下面两个条件,它就是一个堆:

(1)堆是一颗完全二叉树,所以又叫二叉堆

(2)堆中某个节点的值总是不大于(或不小于)其父节点的值。

大顶堆:每个节点的值都大于或等于其子节点的值。 在堆排序算法中用于升序排列;

小顶堆:每个节点的值都小于或等于其子节点的值。 在堆排序算法中用于降序排列;

堆都能用树来表示,一般树的实现都是利用链表

但堆是通过数组来实现的(不是通过链表)

二叉堆易于存储,并且便于索引

二、在堆的实现时,需要注意:

因为是数组,所以父子节点的关系就不需要特殊的结构去维护了,索引之间通过计算就可以得到,省掉了很多麻烦。如果是链表结构,就会复杂很多;

完全二叉树要求叶子节点从左往右填满,才能开始填充下一层,这就保证了不需要对数组整体进行大片的移动。这也是随机存储结构(数组)的短板:删除一个元素之后,整体往前移是比较费时的。这个特性也导致堆在删除元素的时候,要把最后一个叶子节点补充到树根节点的缘由

堆是完全二叉树,所以堆的索引与数组下标一一对应 image.png

二叉堆在数组里,通过当前下标怎么就能找到父节点和子节点呢?

通过Math.floor取索引为整数

  1. 左孩子索引: 2 * index + 1
  2. 右孩子索引: 2 * index + 2
  3. 父节点索引: ( index - 1 ) / 2

堆排序

步骤: 1、 构建最大堆

操作:从最后一个非叶子节点处理,与其孩子节点比较,与其较大的孩子节点 交换位置,只到比较完所有非叶子节点。(上浮)

2、 排序更新堆

操作: (1)、最大堆构建好后,根节点与最后一个叶子节点交换

(2)、交换后更新最大堆的情况(下沉)

(3)、重复(1)、(2)

var len;    // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量

function buildMaxHeap(arr) {   // 建立大顶堆
    len = arr.length;
    for (var i = Math.floor(len/2); i >= 0; i--) {
        heapify(arr, i);
    }
}

function heapify(arr, i) {     // 堆调整
    var left = 2 * i + 1,
        right = 2 * i + 2,
        largest = i;

    if (left < len && arr[left] > arr[largest]) {
        largest = left;
    }

    if (right < len && arr[right] > arr[largest]) {
        largest = right;
    }

    if (largest != i) {
        swap(arr, i, largest);
        heapify(arr, largest);
    }
}

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

function heapSort(arr) {
    buildMaxHeap(arr);

    for (var i = arr.length-1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        heapify(arr, 0);
    }
    return arr;
}
var arr = [1,4,68,20,6,3,9];
console.log(heapSort(arr));