web算法
冒泡排序
1.冒泡排序思路,两两比较一共比较arr.length-1趟
2.每一趟的结果是把最小的排前面,(要做到这个所以要进行arr.length-1-i次比较。)要减去i是因为前面已经排好的就无意义了
3.若前面的数大于后面的数则两者之间进行交换,引入temp。
var arr=[3,2,14,23,1];
for(var i=0;i<arr.length-1;i++){
for(var j=0;j<arr.length-1-i;j++){
if(arr[j]>arr[j+1]){
var temp;
temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
简单选择排序
简单选择排序就是基于冒泡排序进行改进的。提供了一个最小标记,仅仅记录了其坐标,减少了交换次数。
function SelectSort(arr){
var i,j,min,temp;
for(i=0;i<arr.length-1;i++){
min=i;//提供标记,减少交换次数,仅仅记录其坐标信息即可
for(j=i+1;j<arr.length;j++){
if(arr[min]>arr[j]){ //找出arr.length-1-i个中最小的
min=j;
}
}
if(i!=min){
temp=arr[min];
arr[min]=arr[i];
arr[i]=temp;
}
}
}
归并排序
归并排序其实可以类比二分法,二分法其实就是二等分的意思,简而言之就是不断和新序列的中间值进行比较。归并排序似乎有异曲同工之妙,什么意思呢,就是将一个原始序对等分为两部分,然后不断地对等分新的序列,直至序列的长度为1或者2
如果一个序列为1,那就没有比较的意义了,它本身就是之最,如果是两个呢,那直接比较不就完了,把比较之后的值推送到一个新的数组。就这样不断地细分,不断的产生子序列,然后把穿产生的新序列作为新的父序列,然后同等级的父序列再比较产生新的祖序列,依次类推。
第一步:分割
- 获取中间位置
- 根据中间位置,分成左右两边
- 递归执行上一步,直到左右两边只有一个元素为止
第二步:归并(合并有序链表)
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;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(mergeSort(arr));
归并排序不仅仅可以使用于数组,同样可以适用于链表结构
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
示例 1:
输入: 4->2->1->3
输出: 1->2->3->4
解答:采用归并排序
归并排序采用了分治策略,将数组分成2个较小的数组,然后每个数组再分成两个更小的数组,直至每个数组里只包含一个元素,然后将小数组不断的合并成较大的数组,直至只剩下一个数组,就是排序完成后的数组序列。
对应于链表喃?
4->2->1->3
第一步:分割
- 使用快慢指针(双指针法),获取链表的中间节点
- 根据中间节点,分割成两个小链表
- 递归执行上一步,直到小链表中只有一个节点
第二步:归并(合并有序链表)
代码实现
let sortList = function(head) {
return mergeSortRec(head)
}
// 归并排序
// 若分裂后的两个链表长度不为 1,则继续分裂
// 直到分裂后的链表长度都为 1,
// 然后合并小链表
let mergeSortRec = function (head) {
if(!head || !head.next) {
return head
}
// 获取中间节点
let middle = middleNode(head)
// 分裂成两个链表
let temp = middle.next
middle.next = null
let left = head, right = temp
// 继续分裂(递归分裂)
left = mergeSortRec(left)
right = mergeSortRec(right)
// 合并两个有序链表
return mergeTwoLists(left, right)
}
// 获取中间节点
// - 如果链表长度为奇数,则返回中间节点
// - 如果链表长度为偶数,则有两个中间节点,这里返回第一个
let middleNode = function(head) {
let fast = head, slow = head
while(fast && fast.next && fast.next.next) {
slow = slow.next
fast = fast.next.next
}
return slow
}
// 合并两个有序链表
let mergeTwoLists = function(l1, l2) {
let preHead = new ListNode(-1);
let cur = preHead;
while(l1 && l2){
if(l1.val < l2.val){
cur.next = l1;
l1 = l1.next;
}else{
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = l1 || l2;
return preHead.next;
}
引入递归算法的复杂度分析:
- 递归算法的时间复杂度:递归的总次数 * 每次递归的数量
- 递归算法的空间复杂度:递归的深度 * 每次递归创建变量的个数
复杂度分析
- 时间复杂度:递归的总次数为 T(logn) ,每次递归的数量为 T(n) ,时间复杂度为 O(nlogn)
- 空间复杂度:递归的深度为 T(logn) ,每次递归创建变量的个数为 T© (c为常数),空间复杂度为 O(logn)
快速排序
快排使用了分治策略的思想,所谓分治,顾名思义,就是分而治之,将一个复杂的问题,分成两个或多个相似的子问题,在把子问题分成更小的子问题,直到更小的子问题可以简单求解,求解子问题,则原问题的解则为子问题解的合并。
快排的过程简单的说只有三步:
具体按以下步骤实现:
- 1,创建两个指针分别指向数组的最左端以及最右端
- 2,在数组中任意取出一个元素作为基准
- 3,左指针开始向右移动,遇到比基准大的停止
- 4,右指针开始向左移动,遇到比基准小的元素停止,交换左右指针所指向的元素
- 5,重复3,4,直到左指针超过右指针,此时,比基准小的值就都会放在基准的左边,比基准大的值会出现在基准的右边
- 6,然后分别对基准的左右两边重复以上的操作,直到数组完全排序
Math.random()
来随机选取一个数作为基准,下面的代码实现就是以随机数作为基准。
let quickSort = (arr) => {
quick(arr, 0 , arr.length - 1)
}
let quick = (arr, left, right) => {
let index
if(left < right) {
// 划分数组
index = partition(arr, left, right)
if(left < index - 1) {
quick(arr, left, index - 1)
}
if(index < right) {
quick(arr, index, right)
}
}
}
// 一次快排
let partition = (arr, left, right) => {
// 取中间项为基准
var datum = arr[Math.floor(Math.random() * (right - left + 1)) + left],
i = left,
j = right
// 开始调整
while(i <= j) {
// 左指针右移
while(arr[i] < datum) {
i++
}
// 右指针左移
while(arr[j] > datum) {
j--
}
// 交换
if(i <= j) {
swap(arr, i, j)
i += 1
j -= 1
}
}
return i
}
// 交换
let swap = (arr, i , j) => {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
// 测试
let arr = [1, 3, 2, 5, 4]
quickSort(arr)
console.log(arr) // [1, 2, 3, 4, 5]
// 第 2 个最大值
console.log(arr[arr.length - 2]) // 4
插入排序
插入排序
插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入
https://camo.githubusercontent.com/f8b506f83f7649bb4187a6cc02a508cfa8f12bad/687474703a2f2f7265736f757263652e6d757969792e636e2f696d6167652f32303230303730353233303631352e706e67
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MG5qwqpL-1598784491055)(C:\Users\cohhe\Desktop\web算法\插入排序.png)]
function insertionSort(arr) {
let n = arr.length;
let preIndex, current;
for (let i = 1; i < n; 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;
}
整个排序,只需要一个记录的辅助空间,因此,空间复杂度为O(1)。
但是时间复杂度为O(n^2)
希尔排序
1959年Shell发明,第一个突破 O(n2) 的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。
将待排序的数组元素分成多个子序列,使得每个子序列的元素个数相对较少,然后对各个子序列分别进行直接插入排序,待整个待排序列“基本有序”后,最后在对所有元素进行一次直接插入排序。因此,我们要采用跳跃分割的策略:将相距某个“增量”的记录组成一个子序列。
https://img-blog.youkuaiyun.com/20160426213625152
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xEYauapo-1598784491061)(C:\Users\cohhe\Desktop\web算法\20160426213625152.png)]
function shellSort(arr) {
let n = arr.length;
for (let gap = Math.floor(n / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let i = gap; i < n; i++) {
let j = i;
let current = arr[i];
while (j - gap >= 0 && current < arr[j - gap]) {
arr[j] = arr[j - gap];
j = j - gap;
}
arr[j] = current;
}
}
return arr;
}
值得注意的时,希尔排序是一种不稳定的排序
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
柯里化
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
add(1)(2);
// 3
这里定义了一个 add
函数,它接受一个参数并返回一个新的函数。调用 add
之后,返回的函数就通过闭包的方式记住了 add
的第一个参数。所以说 bind
本身也是闭包的一种使用场景。
柯里化函数是一个闭包的应用。通过返回函数来实现将接受多个参数的函数变为接受一个参数的函数。
柯里化是将 f(a,b,c)
可以被以 f(a)(b)(c)
的形式被调用的转化。JavaScript 实现版本通常保留函数被正常调用和在参数数量不够的情况下返回偏函数这两个特性。
简单的说,柯里化函数持续地返回一个新函数直到所有的参数用尽为止。这些参数全部保持“活着”的状态(通过闭包),然后当柯里化链中的最后一个函数被返回和执行时会全部被用来执行
柯里化函数的经典面试题一
1) 需求分析:
实现一个add方法,使计算结果能够满足如下预期
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15
function add(){
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并且收集所有的参数值
var _adder = function(){
//为什么不能用concat数组拼接的方式
_args.push(...arguments);
return _adder;
};
// 利用toString的隐式转换的特性
// 当最后执行的时候进行隐式转换,并进行最终的值返回
_adder.toString = function(){
return _args.reduce(function(a,b){
return a + b;
});
};
return _adder;
}
// 6
console.log( add(1)(2)(3));
// 10
console.log( add(1,2,3)(4));
// 15
console.log( add(1)(2)(3)(4)(5));
= function(){
//为什么不能用concat数组拼接的方式
_args.push(…arguments);
return _adder;
};
// 利用toString的隐式转换的特性
// 当最后执行的时候进行隐式转换,并进行最终的值返回
_adder.toString = function(){
return _args.reduce(function(a,b){
return a + b;
});
};
return _adder;
}
// 6
console.log( add(1)(2)(3));
// 10
console.log( add(1,2,3)(4));
// 15
console.log( add(1)(2)(3)(4)(5));