数据结构与算法之排序和搜索(九)

1,418 阅读4分钟
  • 排序:把某个乱序的数组变成升序或者降序的数组,JS 中的排序:数组的 sort 方法

  • 搜索:找出数组中某个元素的下标,JS 中的搜索:数组的 indexOf 方法。

排序

冒泡排序

  • 比较所有相邻元素,如果第一个比第二个大,则交换它们
  • 一轮下来,可以保证最后一个数是最大的
  • 执行 n-1 轮,就可以完成排序
// 时间复杂度:O(n ^ 2) 两个嵌套循环

Array.prototype.bubbleSort = function() {
  for(let i = 0; i < this.length - 1; i++) {
    for(let j = 0; j < this.length - 1 - i; j++) {
      if(this[j] > this[j + 1]) {
        // 前一个大于后一个就交换值
      	[this[j], this[j + 1]] = [this[j + 1], this[j]]
        // 等价于
        // const temp = this[j];
        // this[j] = this[j + 1];
        // this[j + 1] = temp; 
        
      }
    }
  }
}

const arr = [3, 2, 1, 5, 4];
arr.bubbleSort();
console.log("arr", arr); // [1, 2, 3, 4, 5]

选择排序

  • 找到数组中的最小值,选中它并将其放置在第一位
  • 接着找到第二小的值,选中它并将其放置在第二位
  • 以此类推,执行 n-1 轮
// 时间复杂度:O(n ^ 2) 两个嵌套循环

Array.prototype.selectionSort = function() {
  for(let i = 0; i < this.length - 1; i++) {
    let indexMin = i;

    for(let j = i; j < this.length; j++) {
      // 如果当前这个元素 小于最小值的下标 就更新最小值的下标
      if(this[j] < this[indexMin]) {
        indexMin = j;
      }
    	// 避免自己和自己进行交换位置
      if(i !== indexMin) {
        [this[i], this[indexMin]] = [this[indexMin], this[i]];
      }
    }
  }
}

插入排序

  • 从第二个数开始往前比
  • 比它大就往后排
  • 此此类推进行到最后一个数
// 时间复杂度:O(n ^ 2) 两个嵌套循环

Array.prototype.insertionSort = function() {
  // 从第二个元素开始遍历
  for(let i = 1; i < this.length; i++) {
    const temp = this[i];
    let j = i;

    while(j > 0) {
      // 如果前面的元素大于后面的,往后移动
      if(this[j - 1] > temp) {
        this[j] = this[j - 1];
      }else {
        break;
      }
      j--;
    }

    this[j] = temp;
  }
}

归并排序

思路:

  • 分:把数组劈成两半,再递归地对子数组进行“分〞操作,直到分成一个个单独的数
  • 合:把两个数合并为有序数组,再对有序数组进行合并,直到全部子数组合并为一个完整数组

合并两个有序的数组

  • 新建一个空数组 res,用于存放最终排序后的数组
  • 比较两个有序数组的头部,,较小者出队并推入 res 中
  • 如果两个数组还有值,就重复第二步
// 时间复杂度:O(n * logn) 分需要劈开数组,所以是logn, 合则是n

Array.prototype.mergeSort = function() {
  const rec = (arr) => {
    // 递归终点
    if(arr.length === 1) return arr;
    // 获取中间位置索引
    const mid = Math.floor(arr.length / 2);
    // 获取分割后的左右数组
    const left = arr.slice(0, mid);
    const right = arr.slice(mid);
    // 递归分割
    const leftOrderArr = rec(left);
    const rightOrderArr = rec(right);
    
    const res = [];

    while(leftOrderArr.length || rightOrderArr.length) {
      // 如左边和右边数组都有值
      if(leftOrderArr.length && rightOrderArr.length) {
        res.push(leftOrderArr[0] < rightOrderArr[0] ? leftOrderArr.shift() : rightOrderArr.shift());
      }else if(leftOrderArr.length) {
        // 把左边的队头放入数组
        res.push(leftOrderArr.shift());
      }else if(rightOrderArr.length) {
        // 把右边的队头放入数组
        res.push(rightOrderArr.shift());
      }
    }

    return res;
  }

  const resultArr = rec(this);
  // 把结果赋值原数组
  resultArr.forEach((item, index) => {this[index] = item});
}

快速排序

  • 分区:从数组中任意选择一个“基准”,所有比基准小的元素放在基准前面,比基准大的元素放在基准的后面
  • 递归:递归地对基准前后的子数组进行分区
// 时间复杂度:O(n * logN)  递归的时间复杂度是 O(logN),分区操作的时间复杂度是 O(n)

Array.prototype.quickSort = function() {
    const rec = (arr) => {
      // 如果数组长度小于等于 1 就不用排序了
      if(arr.length <= 1) return arr;
      // 存放基准前后的数组
      const left = [];
      const right = [];
      // 取基准
      const middle = arr[0];
      for(let i = 1; i < arr.length; i++) {
        // 如果当前值小于基准就放到基准前数组里面
        if(arr[i] < middle) {
          left.push(arr[i]);
        }else {
          // 否则就放到基准后数组里面
          right.push(arr[i]);
        }
      }
       // 递归调用两边的子数组
      return [...rec(left), middle, ...rec(right)];
    }

    const res = rec(this);
    res.forEach((item, index) => { this[index] = item })
}

搜索

顺序搜索

  • 遍历数组
  • 找到跟目标值相等的元素,就返回它的下标
Array.prototype.sequentialSearch = function(item) {
  for(let i = 0; i < this.length; i++) {
    if(this[i] === item) {
      return i;
    }
  }
  return -1;
};


console.log(["a", "b", "c"].sequentialSearch("b")); // 1
console.log(["a", "b", "c"].sequentialSearch("d")); // -1

二分搜索

  • 从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束
  • 如果目标值大于或者小于中间元素,则在大于或小于中间元素的那一半数组中搜索
  • 需要保证数组是有序的
// 时间复杂度:O(logN) 每一次比较都使搜索范围缩小一半
// 空间复杂度:O(1)

Array.prototype.binarySearch = function(item) {
  let low = 0;
  let high = this.length - 1;
  
  while(low <= high) {
    const mid = Math.floor((low + high) / 2);
    const midElement = this[mid];
	  // 如果中间元素小于目标值,代表查找值在较大的一半数组
    if(midElement < item) {
      low = mid + 1;
    // 如果中间元素大于目标,代表查找值在较小的一半数组
    }else if(midElement > item) {
      high = mid - 1;
    }else {
      return mid;
    }
  }

  return -1;
};

console.log([1, 2, 3, 4, 5].binarySearch(3)); // 2

LeetCode: 21.合并两个有序链表

  • 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的

解题思路:

  • 与归并排序中的合并两个有序数组很相似
  • 将数组替换成链表就能解此题

解题步骤:

  • 新建一个新链表,作为返回结果
  • 用指针遍历两个有序链表,并比较两个链表的当前节点,较小者先接入新链表,并将指针后移一步
// 时间复杂度:O(m + n) m + n是两个链表长度之和
// 空间复杂度:O(1) 只有指针变量

const mergeTwoLists = function (list1, list2) {
  // 新建链表和三个指针
  const res = new ListNode(0);
  let p = res;
  let p1 = list1;
  let p2 = list2;

  while(p1 && p2) {
    if(p1.val < p2.val) {
      p.next = p1;
      p1 = p1.next;
    } else {
      p.next = p2;
      p2 = p2.next;
    }
  	// 保证永远指向新链表最后一位
    p = p.next;
  }
	// 处理未接入完值的节点
  if(p1) {
    p.next = p1;
  }
  if(p2) {
    p.next = p2;
  }
	// 输出链表头部
  return res.next;
}

LeetCode: 374. 猜数字大小

解题思路:

  • 典型的二分搜索
  • 调用 guess 函数,来判断中间元素是否是目标值

解题步骤:

  • 从数组的中间元素开始,如果中间元素正好是目标值,则搜索过程结束
  • 如果目标值大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找
// 时间复杂度:O(logN) 分割两半一般都是logN
// 空间复杂度:O(1)

const guessNumber = function(n) {
  // 定义最小和最大值
  let low = 1;
  let high = n;

  while(low <= high) {
    const mid = Math.floor((low + high) / 2);
    const res = guess(mid);
    // 猜对了
    if(res === 0) {
      return mid;
    // 猜大了
    }else if(res === 1) {
      low = mid + 1;
    }else {
    	// 猜小了
      high = mid - 1;
    }
  }
};