链表

  function ListNode(val, next) {this.val = (val===undefined ? 0 : val)this.next = (next===undefined ? null : next)}

双指针秒杀7道链表题目

1. 合并两个有序链表

leetcode_21_合并两个有序链表

方法一


function mergeTwoLists(head1, head2) {let newHead = new ListNode(0), p = newHead;while(head1 && head2) {if(head1.val <= head2.val) {p.next = head1;head1 = head1.next;} else {p.next = head2;head2 = head2.next;}p = p.next;}// 没遍历完的接上即可p.next = head1 ? head1 : head2;return newHead.next;
}

方法二

var mergeTwoLists = function(l1, l2) {if(!l1) return l2;if(!l2) return l1;if (l1.val < l2.val) {l1.next = mergeTwoLists(l1.next, l2);return l1;} else {l2.next = mergeTwoLists(l1, l2.next);return l2;}
};

2. 合并 k 个有序链表

var mergeKLists = function(lists) {if(!lists.length) return null; // 空的直接返回,不然 lists.length 是负数return mergeList(lists, 0, lists.length - 1);
};// 归并排序进行不断的分割,return 时的 merge 进行合并排序
function mergeList(lists, start, end) {// 如果 start === end 说明分治的分到头了,只剩一个元素了,直接返回即可if(start === end) {return lists[start];}const mid = start + ((end - start) >> 1); // 找到中点,然后下面继续进行递归分割成左右两部分const leftList = mergeList(lists, start, mid);const rightList = mergeList(lists, mid + 1, end);return merge(leftList, rightList); // 进行合并
}// 这里就是基本算法,合并两个有序链表
function merge(head1, head2) {let newHead = new ListNode(0), p = newHead;while(head1 && head2) {if(head1.val <= head2.val) {p.next = head1;head1 = head1.next;} else {p.next = head2;head2 = head2.next;}p = p.next;}// 没遍历完的接上即可p.next = head1 ? head1 : head2;return newHead.next;
}

3. 寻找单链表的倒数第 k 个节点

// 返回链表的倒数第 k 个节点
function findFromEnd(head, k) {let p1 = head;// p1 先走 k 步for (lett i = 0; i < k; i++) {p1 = p1.next;}let p2 = head;// p1 和 p2 同时走 n - k 步while (p1 != null) {p2 = p2.next;p1 = p1.next;}// p2 现在指向第 n - k 个节点return p2;
}

4. 删除单链表的倒数第k个节点

function removeNthFromEnd(head, n) {// 虚拟头结点let dummy = new ListNode(-1);dummy.next = head;// 删除倒数第 n 个,要先找倒数第 n + 1 个节点let x = findFromEnd(dummy, n + 1);// 删掉倒数第 n 个节点x.next = x.next.next;return dummy.next;
}
// 返回链表的倒数第 k 个节点
function findFromEnd(head, k) {let p1 = head;// p1 先走 k 步for (lett i = 0; i < k; i++) {p1 = p1.next;}let p2 = head;// p1 和 p2 同时走 n - k 步while (p1 != null) {p2 = p2.next;p1 = p1.next;}// p2 现在指向第 n - k 个节点return p2;
}

5. 寻找单链表的中点

function middleNode(head) {// 快慢指针初始化指向 headlet slow = head, fast = head;// 快指针走到末尾时停止while (fast != null && fast.next != null) {// 慢指针走一步,快指针走两步slow = slow.next;fast = fast.next.next;}// 慢指针指向中点return slow;
}

6. 判断单链表是否包含环并找出环起点

// 判断是否包含环
function hasCycle( head) {// 快慢指针初始化指向 headlet slow = head, fast = head;// 快指针走到末尾时停止while (fast != null && fast.next != null) {// 慢指针走一步,快指针走两步slow = slow.next;fast = fast.next.next;// 快慢指针相遇,说明含有环if (slow == fast) {return true;}}// 不包含环return false;
}
// 包含环,找出环的起点
ListNode detectCycle(ListNode head) {ListNode fast, slow;fast = slow = head;while (fast != null && fast.next != null) {fast = fast.next.next;slow = slow.next;if (fast == slow) break;}// 上面的代码类似 hasCycle 函数if (fast == null || fast.next == null) {// fast 遇到空指针说明没有环return null;}// 重新指向头结点slow = head;// 快慢指针同步前进,相交点就是环起点while (slow != fast) {fast = fast.next;slow = slow.next;}return slow;
}

7. 判断两个单链表是否相交并找出交点

ListNode getIntersectionNode(ListNode headA, ListNode headB) {// p1 指向 A 链表头结点,p2 指向 B 链表头结点ListNode p1 = headA, p2 = headB;while (p1 != p2) {// p1 走一步,如果走到 A 链表末尾,转到 B 链表if (p1 == null) p1 = headB;else            p1 = p1.next;// p2 走一步,如果走到 B 链表末尾,转到 A 链表if (p2 == null) p2 = headA;else            p2 = p2.next;}return p1;
}

反转链表

反转整个单链表

// 定义:输入一个单链表头结点,将该链表反转,返回新的头结点
ListNode reverse(ListNode head) {if (head == null || head.next == null) {return head;}ListNode last = reverse(head.next);head.next.next = head;head.next = null;return last;
}

反转链表的前n个节点

ListNode successor = null; // 后驱节点// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {if (n == 1) {// 记录第 n + 1 个节点successor = head.next;return head;}// 以 head.next 为起点,需要反转前 n - 1 个节点ListNode last = reverseN(head.next, n - 1);head.next.next = head;// 让反转之后的 head 节点和后面的节点连起来head.next = successor;return last;
}

反转给定链表索引区间 [m, n]的一部分

ListNode reverseBetween(ListNode head, int m, int n) {// base caseif (m == 1) {return reverseN(head, n);}// 前进到反转的起点触发 base casehead.next = reverseBetween(head.next, m - 1, n - 1);return head;
}

数组

快慢指针技巧——原地修改数组

1. 删除有序数组中的重复项

int removeDuplicates(int[] nums) {if (nums.length == 0) {return 0;}int slow = 0, fast = 0;while (fast < nums.length) {if (nums[fast] != nums[slow]) {slow++;// 维护 nums[0..slow] 无重复nums[slow] = nums[fast];}fast++;}// 数组长度为索引 + 1return slow + 1;
}

2. 移除元素

int removeElement(int[] nums, int val) {int fast = 0, slow = 0;while (fast < nums.length) {if (nums[fast] != val) {nums[slow] = nums[fast];slow++;}fast++;}return slow;
}

3. 移动零

void moveZeroes(int[] nums) {// 去除 nums 中的所有 0,返回不含 0 的数组长度int p = removeElement(nums, 0);// 将 nums[p..] 的元素赋值为 0for (; p < nums.length; p++) {nums[p] = 0;}
}// 见上文代码实现
int removeElement(int[] nums, int val);

快慢指针技巧——滑动窗口

1. 最小覆盖字串

string minWindow(string s, string t) {unordered_map<char, int> need, window;for (char c : t) need[c]++;int left = 0, right = 0;int valid = 0;// 记录最小覆盖子串的起始索引及长度int start = 0, len = INT_MAX;while (right < s.size()) {// c 是将移入窗口的字符char c = s[right];// 扩大窗口right++;// 进行窗口内数据的一系列更新if (need.count(c)) {window[c]++;if (window[c] == need[c])valid++;}// 判断左侧窗口是否要收缩while (valid == need.size()) {// 在这里更新最小覆盖子串if (right - left < len) {start = left;len = right - left;}// d 是将移出窗口的字符char d = s[left];// 缩小窗口left++;// 进行窗口内数据的一系列更新if (need.count(d)) {if (window[d] == need[d])valid--;window[d]--;}                    }}// 返回最小覆盖子串return len == INT_MAX ?"" : s.substr(start, len);
}

2. 字符串排列

// 判断 s 中是否存在 t 的排列
bool checkInclusion(string t, string s) {unordered_map<char, int> need, window;for (char c : t) need[c]++;int left = 0, right = 0;int valid = 0;while (right < s.size()) {char c = s[right];right++;// 进行窗口内数据的一系列更新if (need.count(c)) {window[c]++;if (window[c] == need[c])valid++;}// 判断左侧窗口是否要收缩while (right - left >= t.size()) {// 在这里判断是否找到了合法的子串if (valid == need.size())return true;char d = s[left];left++;// 进行窗口内数据的一系列更新if (need.count(d)) {if (window[d] == need[d])valid--;window[d]--;}}}// 未找到符合条件的子串return false;
}

3. 找出所有字母异位词

vector<int> findAnagrams(string s, string t) {unordered_map<char, int> need, window;for (char c : t) need[c]++;int left = 0, right = 0;int valid = 0;vector<int> res; // 记录结果while (right < s.size()) {char c = s[right];right++;// 进行窗口内数据的一系列更新if (need.count(c)) {window[c]++;if (window[c] == need[c]) valid++;}// 判断左侧窗口是否要收缩while (right - left >= t.size()) {// 当窗口符合条件时,把起始索引加入 resif (valid == need.size())res.push_back(left);char d = s[left];left++;// 进行窗口内数据的一系列更新if (need.count(d)) {if (window[d] == need[d])valid--;window[d]--;}}}return res;
}

4. 最长无重复子串

int lengthOfLongestSubstring(string s) {unordered_map<char, int> window;int left = 0, right = 0;int res = 0; // 记录结果while (right < s.size()) {char c = s[right];right++;// 进行窗口内数据的一系列更新window[c]++;// 判断左侧窗口是否要收缩while (window[c] > 1) {char d = s[left];left++;// 进行窗口内数据的一系列更新window[d]--;}// 在这里更新答案res = max(res, right - left);}return res;
}

左右指针——(由外向内)

1. 求和

1.1 两数之和

int[] twoSum(int[] nums, int target) {// 一左一右两个指针相向而行int left = 0, right = nums.length - 1;while (left < right) {int sum = nums[left] + nums[right];if (sum == target) {// 题目要求的索引是从 1 开始的return new int[]{left + 1, right + 1};} else if (sum < target) {left++; // 让 sum 大一点} else if (sum > target) {right--; // 让 sum 小一点}}return new int[]{-1, -1};
}

1.2 两数之和——进阶

nums 中可能有多对儿元素之和都等于 target,请你的算法返回所有和为 target 的元素对儿,其中不能出现重复。

过程:可以实现,但是会重复

vector<vector<int>> twoSumTarget(vector<int>& nums, int target {// 先对数组排序sort(nums.begin(), nums.end());vector<vector<int>> res;int lo = 0, hi = nums.size() - 1;while (lo < hi) {int sum = nums[lo] + nums[hi];// 根据 sum 和 target 的比较,移动左右指针if      (sum < target) lo++;else if (sum > target) hi--;else {res.push_back({lo, hi});lo++; hi--;}}return res;
}

结果:跳过重复的元素

 // 先对数组排序sort(nums.begin(), nums.end());vector<vector<int>> res;int lo = 0, hi = nums.size() - 1;
while (lo < hi) {int sum = nums[lo] + nums[hi];// 记录索引 lo 和 hi 最初对应的值int left = nums[lo], right = nums[hi];if (sum < target)      lo++;else if (sum > target) hi--;else {res.push_back({left, right});// 跳过所有重复的元素while (lo < hi && nums[lo] == left) lo++;while (lo < hi && nums[hi] == right) hi--;}
}

1.3 三数之和

/* 从 nums[start] 开始,计算有序数组* nums 中所有和为 target 的二元组 */
vector<vector<int>> twoSumTarget(vector<int>& nums, int start, int target) {// 左指针改为从 start 开始,其他不变int lo = start, hi = nums.size() - 1;vector<vector<int>> res;while (lo < hi) {...}return res;
}/* 计算数组 nums 中所有和为 target 的三元组 */
vector<vector<int>> threeSumTarget(vector<int>& nums, int target) {// 数组得排个序sort(nums.begin(), nums.end());int n = nums.size();vector<vector<int>> res;// 穷举 threeSum 的第一个数for (int i = 0; i < n; i++) {// 对 target - nums[i] 计算 twoSumvector<vector<int>> tuples = twoSumTarget(nums, i + 1, target - nums[i]);// 如果存在满足条件的二元组,再加上 nums[i] 就是结果三元组for (vector<int>& tuple : tuples) {tuple.push_back(nums[i]);res.push_back(tuple);}// 跳过第一个数字重复的情况,否则会出现重复结果while (i < n - 1 && nums[i] == nums[i + 1]) i++;}return res;
}

1.4 四数之和

vector<vector<int>> fourSum(vector<int>& nums, int target) {// 数组需要排序sort(nums.begin(), nums.end());int n = nums.size();vector<vector<int>> res;// 穷举 fourSum 的第一个数for (int i = 0; i < n; i++) {// 对 target - nums[i] 计算 threeSumvector<vector<int>> triples = threeSumTarget(nums, i + 1, target - nums[i]);// 如果存在满足条件的三元组,再加上 nums[i] 就是结果四元组for (vector<int>& triple : triples) {triple.push_back(nums[i]);res.push_back(triple);}// fourSum 的第一个数不能重复while (i < n - 1 && nums[i] == nums[i + 1]) i++;}return res;
}/* 从 nums[start] 开始,计算有序数组* nums 中所有和为 target 的三元组 */
vector<vector<int>> threeSumTarget(vector<int>& nums, int start, int target) {int n = nums.size();vector<vector<int>> res;// i 从 start 开始穷举,其他都不变for (int i = start; i < n; i++) {...}return res;

1.5 n数之和

/* 注意:调用这个函数之前一定要先给 nums 排序 */
vector<vector<int>> nSumTarget(vector<int>& nums, int n, int start, int target) {int sz = nums.size();vector<vector<int>> res;// 至少是 2Sum,且数组大小不应该小于 nif (n < 2 || sz < n) return res;// 2Sum 是 base caseif (n == 2) {// 双指针那一套操作int lo = start, hi = sz - 1;while (lo < hi) {int sum = nums[lo] + nums[hi];int left = nums[lo], right = nums[hi];if (sum < target) {while (lo < hi && nums[lo] == left) lo++;} else if (sum > target) {while (lo < hi && nums[hi] == right) hi--;} else {res.push_back({left, right});while (lo < hi && nums[lo] == left) lo++;while (lo < hi && nums[hi] == right) hi--;}}} else {// n > 2 时,递归计算 (n-1)Sum 的结果for (int i = start; i < sz; i++) {vector<vector<int>> sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);for (vector<int>& arr : sub) {// (n-1)Sum 加上 nums[i] 就是 nSumarr.push_back(nums[i]);res.push_back(arr);}while (i < sz - 1 && nums[i] == nums[i + 1]) i++;}}return res;
}

2. 反转数组

void reverseString(char[] s) {// 一左一右两个指针相向而行int left = 0, right = s.length - 1;while (left < right) {// 交换 s[left] 和 s[right]char temp = s[left];s[left] = s[right];s[right] = temp;left++;right--;}
}

3. 回文串判断

boolean isPalindrome(String s) {// 一左一右两个指针相向而行int left = 0, right = s.length() - 1;while (left < right) {if (s.charAt(left) != s.charAt(right)) {return false;}left++;right--;}return true;
}

4. 寻找最长回文子串——双指针(由内向外)

// 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串
String palindrome(String s, int l, int r) {// 防止索引越界while (l >= 0 && r < s.length()&& s.charAt(l) == s.charAt(r)) {// 双指针,向两边展开l--; r++;}// 返回以 s[l] 和 s[r] 为中心的最长回文串return s.substring(l + 1, r);
}
String longestPalindrome(String s) {String res = "";for (int i = 0; i < s.length(); i++) {// 以 s[i] 为中心的最长回文子串String s1 = palindrome(s, i, i);// 以 s[i] 和 s[i+1] 为中心的最长回文子串String s2 = palindrome(s, i, i + 1);// res = longest(res, s1, s2)res = res.length() > s1.length() ? res : s1;res = res.length() > s2.length() ? res : s2;}return res;
}

二叉树

1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个 traverse 函数配合外部变量来实现,这叫「遍历」的思维模式。
2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。

纲领篇

1. 二叉树的最大深度


方法一

// 记录最大深度
int res = 0;
// 记录遍历到的节点的深度
int depth = 0;// 主函数
int maxDepth(TreeNode root) {traverse(root);return res;
}// 二叉树遍历框架
void traverse(TreeNode root) {if (root == null) {return;}// 前序位置depth++;if (root.left == null && root.right == null) {// 到达叶子节点,更新最大深度res = Math.max(res, depth);}traverse(root.left);traverse(root.right);// 后序位置depth--;
}

方法二

分解问题计算答案

// 定义:输入根节点,返回这棵二叉树的最大深度
int maxDepth(TreeNode root) {if (root == null) {return 0;}// 利用定义,计算左右子树的最大深度int leftMax = maxDepth(root.left);int rightMax = maxDepth(root.right);// 整棵树的最大深度等于左右子树的最大深度取最大值,// 然后再加上根节点自己int res = Math.max(leftMax, rightMax) + 1;return res;
}

2. 二叉树的直径


方法一:最坏时间复杂度是 O(N^2)

// 记录最大直径的长度
int maxDiameter = 0;public int diameterOfBinaryTree(TreeNode root) {// 对每个节点计算直径,求最大直径traverse(root);return maxDiameter;
}// 遍历二叉树
void traverse(TreeNode root) {if (root == null) {return;}// 对每个节点计算直径int leftMax = maxDepth(root.left);int rightMax = maxDepth(root.right);int myDiameter = leftMax + rightMax;// 更新全局最大直径maxDiameter = Math.max(maxDiameter, myDiameter);traverse(root.left);traverse(root.right);
}// 计算二叉树的最大深度
int maxDepth(TreeNode root) {if (root == null) {return 0;}int leftMax = maxDepth(root.left);int rightMax = maxDepth(root.right);return 1 + Math.max(leftMax, rightMax);
}

方法二:时间复杂度只有 O(N)

// 记录最大直径的长度
int maxDiameter = 0;public int diameterOfBinaryTree(TreeNode root) {maxDepth(root);return maxDiameter;
}int maxDepth(TreeNode root) {if (root == null) {return 0;}int leftMax = maxDepth(root.left);int rightMax = maxDepth(root.right);// 后序位置,顺便计算最大直径int myDiameter = leftMax + rightMax;maxDiameter = Math.max(maxDiameter, myDiameter);return 1 + Math.max(leftMax, rightMax);
}

3. 层序遍历

// 输入一棵二叉树的根节点,层序遍历这棵二叉树
void levelTraverse(TreeNode root) {if (root == null) return;Queue<TreeNode> q = new LinkedList<>();q.offer(root);// 从上到下遍历二叉树的每一层while (!q.isEmpty()) {int sz = q.size();// 从左到右遍历每一层的每个节点for (int i = 0; i < sz; i++) {TreeNode cur = q.poll();// 将下一层节点放入队列if (cur.left != null) {q.offer(cur.left);}if (cur.right != null) {q.offer(cur.right);}}}
}

思路篇

1. 翻转二叉树


方法一:遍历的方式

// 主函数
TreeNode invertTree(TreeNode root) {// 遍历二叉树,交换每个节点的子节点traverse(root);return root;
}// 二叉树遍历函数
void traverse(TreeNode root) {if (root == null) {return;}/**** 前序位置 ****/// 每一个节点需要做的事就是交换它的左右子节点TreeNode tmp = root.left;root.left = root.right;root.right = tmp;// 遍历框架,去遍历左右子树的节点traverse(root.left);traverse(root.right);
}

方法二: 分解问题

// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点
TreeNode invertTree(TreeNode root) {if (root == null) {return null;}// 利用函数定义,先翻转左右子树TreeNode left = invertTree(root.left);TreeNode right = invertTree(root.right);// 然后交换左右子节点root.left = right;root.right = left;// 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 rootreturn root;
}

2. 填充每个二叉树节点的右侧指针

// 主函数
Node connect(Node root) {if (root == null) return null;// 遍历「三叉树」,连接相邻节点traverse(root.left, root.right);return root;
}// 三叉树遍历框架
void traverse(Node node1, Node node2) {if (node1 == null || node2 == null) {return;}/**** 前序位置 ****/// 将传入的两个节点穿起来node1.next = node2;// 连接相同父节点的两个子节点traverse(node1.left, node1.right);traverse(node2.left, node2.right);// 连接跨越父节点的两个子节点traverse(node1.right, node2.left);
}

3. 将二叉树展开为列表

// 定义:将以 root 为根的树拉平为链表
void flatten(TreeNode root) {// base caseif (root == null) return;// 利用定义,把左右子树拉平flatten(root.left);flatten(root.right);/**** 后序遍历位置 ****/// 1、左右子树已经被拉平成一条链表TreeNode left = root.left;TreeNode right = root.right;// 2、将左子树作为右子树root.left = null;root.right = left;// 3、将原先的右子树接到当前右子树的末端TreeNode p = root;while (p.right != null) {p = p.right;}p.right = right;
}

构造篇

1. 最大二叉树

/* 主函数 */
TreeNode constructMaximumBinaryTree(int[] nums) {return build(nums, 0, nums.length - 1);
}// 定义:将 nums[lo..hi] 构造成符合条件的树,返回根节点
TreeNode build(int[] nums, int lo, int hi) {// base caseif (lo > hi) {return null;}// 找到数组中的最大值和对应的索引int index = -1, maxVal = Integer.MIN_VALUE;for (int i = lo; i <= hi; i++) {if (maxVal < nums[i]) {index = i;maxVal = nums[i];}}// 先构造出根节点TreeNode root = new TreeNode(maxVal);// 递归调用构造左右子树root.left = build(nums, lo, index - 1);root.right = build(nums, index + 1, hi);return root;
}

2. 通过前序和中序遍历结果构造二叉树


TreeNode build(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {if (preStart > preEnd) {return null;}// root 节点对应的值就是前序遍历数组的第一个元素int rootVal = preorder[preStart];// rootVal 在中序遍历数组中的索引int index = valToIndex.get(rootVal);int leftSize = index - inStart;// 先构造出当前根节点TreeNode root = new TreeNode(rootVal);// 递归构造左右子树root.left = build(preorder, preStart + 1, preStart + leftSize,inorder, inStart, index - 1);root.right = build(preorder, preStart + leftSize + 1, preEnd,inorder, index + 1, inEnd);return root;
}

3. 通过前序和中序遍历结果构造二叉树


![在这里插入图片描述](https://img-blog.csdnimg.cn/d6dd19d7ca80464eada28f374ebcb657.png

// 存储 inorder 中值到索引的映射
HashMap<Integer, Integer> valToIndex = new HashMap<>();TreeNode buildTree(int[] inorder, int[] postorder) {for (int i = 0; i < inorder.length; i++) {valToIndex.put(inorder[i], i);}return build(inorder, 0, inorder.length - 1,postorder, 0, postorder.length - 1);
}/* build 函数的定义:后序遍历数组为 postorder[postStart..postEnd],中序遍历数组为 inorder[inStart..inEnd],构造二叉树,返回该二叉树的根节点
*/
TreeNode build(int[] inorder, int inStart, int inEnd,int[] postorder, int postStart, int postEnd) {// root 节点对应的值就是后序遍历数组的最后一个元素int rootVal = postorder[postEnd];// rootVal 在中序遍历数组中的索引int index = valToIndex.get(rootVal);TreeNode root = new TreeNode(rootVal);// 递归构造左右子树root.left = build(inorder, inStart, index - 1,postorder, postStart, postStart + leftSize - 1);root.right = build(inorder, index + 1, inEnd,postorder, postStart + leftSize, postEnd - 1);return root;
}

4. 通过后序和前序遍历结果构造二叉树



class Solution {// 存储 postorder 中值到索引的映射HashMap<Integer, Integer> valToIndex = new HashMap<>();public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {for (int i = 0; i < postorder.length; i++) {valToIndex.put(postorder[i], i);}return build(preorder, 0, preorder.length - 1,postorder, 0, postorder.length - 1);}// 定义:根据 preorder[preStart..preEnd] 和 postorder[postStart..postEnd]// 构建二叉树,并返回根节点。TreeNode build(int[] preorder, int preStart, int preEnd,int[] postorder, int postStart, int postEnd) {if (preStart > preEnd) {return null;}if (preStart == preEnd) {return new TreeNode(preorder[preStart]);}// root 节点对应的值就是前序遍历数组的第一个元素int rootVal = preorder[preStart];// root.left 的值是前序遍历第二个元素// 通过前序和后序遍历构造二叉树的关键在于通过左子树的根节点// 确定 preorder 和 postorder 中左右子树的元素区间int leftRootVal = preorder[preStart + 1];// leftRootVal 在后序遍历数组中的索引int index = valToIndex.get(leftRootVal);// 左子树的元素个数int leftSize = index - postStart + 1;// 先构造出当前根节点TreeNode root = new TreeNode(rootVal);// 递归构造左右子树// 根据左子树的根节点索引和元素个数推导左右子树的索引边界root.left = build(preorder, preStart + 1, preStart + leftSize,postorder, postStart, index);root.right = build(preorder, preStart + leftSize + 1, preEnd,postorder, index + 1, postEnd - 1);return root;}
}

基本操作

二叉搜索树操作的代码框架

void BST(TreeNode root, int target) {if (root.val == target)// 找到目标,做点什么if (root.val < target) BST(root.right, target);if (root.val > target)BST(root.left, target);
}

1. 验证二叉搜索树

const helper = (root, lower, upper) => {if (root === null) {return true;}if (root.val <= lower || root.val >= upper) {return false;}return helper(root.left, lower, root.val) && helper(root.right, root.val, upper);
}
var isValidBST = function(root) {return helper(root, -Infinity, Infinity);
};

2. 普通树中搜索值为target的节点

TreeNode searchBST(TreeNode root, int target);if (root == null) return null;if (root.val == target) return root;// 当前节点没找到就递归地去左右子树寻找TreeNode left = searchBST(root.left, target);TreeNode right = searchBST(root.right, target);return left != null ? left : right;
}

3. 二叉搜索树中搜索

TreeNode searchBST(TreeNode root, int target) {if (root == null) {return null;}// 去左子树搜索if (root.val > target) {return searchBST(root.left, target);}// 去右子树搜索if (root.val < target) {return searchBST(root.right, target);}return root;
}

4. 二叉搜索树中插入一个数

TreeNode searchBST(TreeNode root, int target) {if (root == null) {return null;}// 去左子树搜索if (root.val > target) {return searchBST(root.left, target);}// 去右子树搜索if (root.val < target) {return searchBST(root.right, target);}return root;
}

5. 二叉搜索树中删除一个数

需要考虑三种情况


TreeNode deleteNode(TreeNode root, int key) {if (root == null) return null;if (root.val == key) {// 这两个 if 把情况 1 和 2 都正确处理了if (root.left == null) return root.right;if (root.right == null) return root.left;// 处理情况 3// 获得右子树最小的节点TreeNode minNode = getMin(root.right);// 删除右子树最小的节点root.right = deleteNode(root.right, minNode.val);// 用右子树最小的节点替换 root 节点minNode.left = root.left;minNode.right = root.right;root = minNode;} else if (root.val > key) {root.left = deleteNode(root.left, key);} else if (root.val < key) {root.right = deleteNode(root.right, key);}return root;
}TreeNode getMin(TreeNode node) {// BST 最左边的就是最小的while (node.left != null) node = node.left;return node;
}

动态规划

套路框架

1. 斐波那契数列

int fib(int N) {// 备忘录全初始化为 0int[] memo = new int[N + 1];// 进行带备忘录的递归return helper(memo, N);
}int helper(int[] memo, int n) {// base caseif (n == 0 || n == 1) return n;// 已经计算过,不用再计算了if (memo[n] != 0) return memo[n];memo[n] = helper(memo, n - 1) + helper(memo, n - 2);return memo[n];
}

优化

int fib(int n) {if (n == 0 || n == 1) {// base casereturn n;}// 分别代表 dp[i - 1] 和 dp[i - 2]int dp_i_1 = 1, dp_i_2 = 0;for (int i = 2; i <= n; i++) {// dp[i] = dp[i - 1] + dp[i - 2];int dp_i = dp_i_1 + dp_i_2;// 滚动更新dp_i_2 = dp_i_1;dp_i_1 = dp_i;}return dp_i_1;
}

2. 零钱兑换

int[] memo;int coinChange(int[] coins, int amount) {memo = new int[amount + 1];// 备忘录初始化为一个不会被取到的特殊值,代表还未被计算Arrays.fill(memo, -666);return dp(coins, amount);
}
// 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币
int dp(int[] coins, int amount) {if (amount == 0) return 0;if (amount < 0) return -1;// 查备忘录,防止重复计算if (memo[amount] != -666)return memo[amount];int res = Integer.MAX_VALUE;for (int coin : coins) {// 计算子问题的结果int subProblem = dp(coins, amount - coin);// 子问题无解则跳过if (subProblem == -1) continue;// 在子问题中选择最优解,然后加一res = Math.min(res, subProblem + 1);}// 把计算结果存入备忘录memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res;return memo[amount];
}

子序列类型问题

1. 最长递增子序列

int lengthOfLIS(int[] nums) {// 定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度int[] dp = new int[nums.length];// base case:dp 数组全都初始化为 1Arrays.fill(dp, 1);for (int i = 0; i < nums.length; i++) {for (int j = 0; j < i; j++) {if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);}}int res = 0;for (int i = 0; i < dp.length; i++) {res = Math.max(res, dp[i]);}return res;
}

2. 俄罗斯套娃信封问题


先对宽度 w 进行升序排序,如果遇到 w 相同的情况,则按照高度 h 降序排序;之后把所有的 h 作为一个数组,在这个数组上计算 LIS 的长度就是答案。

// envelopes = [[w, h], [w, h]...]
public int maxEnvelopes(int[][] envelopes) {int n = envelopes.length;// 按宽度升序排列,如果宽度一样,则按高度降序排列Arrays.sort(envelopes, new Comparator<int[]>() {public int compare(int[] a, int[] b) {return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0];}});// 对高度数组寻找 LISint[] height = new int[n];for (int i = 0; i < n; i++)height[i] = envelopes[i][1];return lengthOfLIS(height);
}int lengthOfLIS(int[] nums) {// 定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度int[] dp = new int[nums.length];// base case:dp 数组全都初始化为 1Arrays.fill(dp, 1);for (int i = 0; i < nums.length; i++) {for (int j = 0; j < i; j++) {if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);}}int res = 0;for (int i = 0; i < dp.length; i++) {res = Math.max(res, dp[i]);}return res;
}

3. 最大子数组和

int maxSubArray(int[] nums) {int n = nums.length;if (n == 0) return 0;// 定义:dp[i] 记录以 nums[i] 为结尾的「最大子数组和」int[] dp = new int[n];// base case// 第一个元素前面没有子数组dp[0] = nums[0];// 状态转移方程for (int i = 1; i < n; i++) {// 要么自成一派,要么和前面的子数组合并dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);}// 得到 nums 的最大子数组int res = Integer.MIN_VALUE;for (int i = 0; i < n; i++) {res = Math.max(res, dp[i]);}return res;
}

优化–降低空间复杂度

int maxSubArray(int[] nums) {int n = nums.length;if (n == 0) return 0;// base caseint dp_0 = nums[0];int dp_1 = 0, res = dp_0;for (int i = 1; i < n; i++) {// dp[i] = max(nums[i], nums[i] + dp[i-1])dp_1 = Math.max(nums[i], nums[i] + dp_0);dp_0 = dp_1;// 顺便计算最大的结果res = Math.max(res, dp_1);}return res;
}

4. 最长公共子序列类型

4.1 最长公共子序列

// 备忘录,消除重叠子问题
int[][] memo;/* 主函数 */
int longestCommonSubsequence(String s1, String s2) {int m = s1.length(), n = s2.length();// 备忘录值为 -1 代表未曾计算memo = new int[m][n];for (int[] row : memo) Arrays.fill(row, -1);// 计算 s1[0..] 和 s2[0..] 的 lcs 长度return dp(s1, 0, s2, 0);
}// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度
int dp(String s1, int i, String s2, int j) {// base caseif (i == s1.length() || j == s2.length()) {return 0;}// 如果之前计算过,则直接返回备忘录中的答案if (memo[i][j] != -1) {return memo[i][j];}// 根据 s1[i] 和 s2[j] 的情况做选择if (s1.charAt(i) == s2.charAt(j)) {// s1[i] 和 s2[j] 必然在 lcs 中memo[i][j] = 1 + dp(s1, i + 1, s2, j + 1);} else {// s1[i] 和 s2[j] 至少有一个不在 lcs 中memo[i][j] = Math.max(dp(s1, i + 1, s2, j),dp(s1, i, s2, j + 1));}return memo[i][j];
}

4.2 两个字符串的删除操作


那么,要计算删除的次数,就可以通过最长公共子序列的长度推导出来

int minDistance(String s1, String s2) {int m = s1.length(), n = s2.length();// 复用前文计算 lcs 长度的函数int lcs = longestCommonSubsequence(s1, s2);return m - lcs + n - lcs;
}

4.3 最小ascii删除和

// 备忘录
int memo[][];
/* 主函数 */
int minimumDeleteSum(String s1, String s2) {int m = s1.length(), n = s2.length();// 备忘录值为 -1 代表未曾计算memo = new int[m][n];for (int[] row : memo) Arrays.fill(row, -1);return dp(s1, 0, s2, 0);
}// 定义:将 s1[i..] 和 s2[j..] 删除成相同字符串,
// 最小的 ASCII 码之和为 dp(s1, i, s2, j)。
int dp(String s1, int i, String s2, int j) {int res = 0;// base caseif (i == s1.length()) {// 如果 s1 到头了,那么 s2 剩下的都得删除for (; j < s2.length(); j++)res += s2.charAt(j);return res;}if (j == s2.length()) {// 如果 s2 到头了,那么 s1 剩下的都得删除for (; i < s1.length(); i++)res += s1.charAt(i);return res;}if (memo[i][j] != -1) {return memo[i][j];}if (s1.charAt(i) == s2.charAt(j)) {// s1[i] 和 s2[j] 都是在 lcs 中的,不用删除memo[i][j] = dp(s1, i + 1, s2, j + 1);} else {// s1[i] 和 s2[j] 至少有一个不在 lcs 中,删一个memo[i][j] = Math.min(s1.charAt(i) + dp(s1, i + 1, s2, j),s2.charAt(j) + dp(s1, i, s2, j + 1));}return memo[i][j];
}

背包类型问题

1. 0-1背包问题


dp[i][w] 表示:对于前 i 个物品(从 1 开始计数),当前背包的容量为 w 时,这种情况下可以装下的最大价值是 dp[i][w]。

如果你没有把这第 i 个物品装入背包,那么很显然,最大价值 dp[i][w] 应该等于 dp[i-1][w],继承之前的结果。

如果你把这第 i 个物品装入了背包,那么 dp[i][w] 应该等于 val[i-1] + dp[i-1][w - wt[i-1]]。

首先,由于数组索引从 0 开始,而我们定义中的 i 是从 1 开始计数的,所以 val[i-1] 和 wt[i-1] 表示第 i 个物品的价值和重量。

你如果选择将第 i 个物品装进背包,那么第 i 个物品的价值 val[i-1] 肯定就到手了,接下来你就要在剩余容量 w - wt[i-1] 的限制下,在前 i - 1 个物品中挑选,求最大价值,即 dp[i-1][w - wt[i-1]]。

int knapsack(int W, int N, int[] wt, int[] val) {// base case 已初始化int[][] dp = new int[N + 1][W + 1];for (int i = 1; i <= N; i++) {for (int w = 1; w <= W; w++) {if (w - wt[i - 1] < 0) {// 这种情况下只能选择不装入背包dp[i][w] = dp[i - 1][w];} else {// 装入或者不装入背包,择优dp[i][w] = Math.max(dp[i - 1][w - wt[i-1]] + val[i-1], dp[i - 1][w]);}}}return dp[N][W];
}

2. 分割等和子集


我们可以先对集合求和,得出 sum,把问题转化为背包问题:

给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为 nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?

dp[i][j] = x 表示,对于前 i 个物品(i 从 1 开始计数),当前背包的容量为 j 时,若 x 为 true,则说明可以恰好将背包装满,若 x 为 false,则说明不能恰好将背包装满。

boolean canPartition(int[] nums) {int sum = 0;for (int num : nums) sum += num;// 和为奇数时,不可能划分成两个和相等的集合if (sum % 2 != 0) return false;int n = nums.length;sum = sum / 2;boolean[][] dp = new boolean[n + 1][sum + 1];// base casefor (int i = 0; i <= n; i++)dp[i][0] = true;for (int i = 1; i <= n; i++) {for (int j = 1; j <= sum; j++) {if (j - nums[i - 1] < 0) {// 背包容量不足,不能装入第 i 个物品dp[i][j] = dp[i - 1][j];} else {// 装入或不装入背包dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];}}}return dp[n][sum];
}

3. 零钱兑换


首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维 dp 数组。

dp[i][j] 的定义如下:

若只使用前 i 个物品(可以重复使用),当背包容量为 j 时,有 dp[i][j] 种方法可以装满背包。

换句话说,翻译回我们题目的意思就是:

若只使用 coins 中的前 i 个(i 从 1 开始计数)硬币的面值,若想凑出金额 j,有 dp[i][j] 种凑法。

经过以上的定义,可以得到:

base case 为 dp[0][…] = 0, dp[…][0] = 1。因为如果不使用任何硬币面值,就无法凑出任何金额;如果凑出的目标金额为 0,那么“无为而治”就是唯一的一种凑法。

我们最终想得到的答案就是 dp[N][amount],其中 N 为 coins 数组的大小。

如果你不把这第 i 个物品装入背包,也就是说你不使用 coins[i-1] 这个面值的硬币,那么凑出面额 j 的方法数 dp[i][j] 应该等于 dp[i-1][j],继承之前的结果。

如果你把这第 i 个物品装入了背包,也就是说你使用 coins[i-1] 这个面值的硬币,那么 dp[i][j] 应该等于 dp[i][j-coins[i-1]]。

int change(int amount, int[] coins) {int n = coins.length;int[][] dp = int[n + 1][amount + 1];// base casefor (int i = 0; i <= n; i++) dp[i][0] = 1;for (int i = 1; i <= n; i++) {for (int j = 1; j <= amount; j++)if (j - coins[i-1] >= 0)dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i-1]];else dp[i][j] = dp[i - 1][j];}return dp[n][amount];
}

最小路径和


int[][] memo;int minPathSum(int[][] grid) {int m = grid.length;int n = grid[0].length;// 构造备忘录,初始值全部设为 -1memo = new int[m][n];for (int[] row : memo)Arrays.fill(row, -1);return dp(grid, m - 1, n - 1);
}int dp(int[][] grid, int i, int j) {// base caseif (i == 0 && j == 0) {return grid[0][0];}if (i < 0 || j < 0) {return Integer.MAX_VALUE;}// 避免重复计算if (memo[i][j] != -1) {return memo[i][j];}// 将计算结果记入备忘录memo[i][j] = Math.min(dp(grid, i - 1, j),dp(grid, i, j - 1)) + grid[i][j];return memo[i][j];
}

一个方法团灭 LEETCODE 打家劫舍问题

一个方法团灭 LEETCODE 股票买卖问题

排序类

1. 冒泡排序类

1. 冒泡排序+优化

// 第一版
let bubbleSort = arr => {for (let i = 0; i < arr.length; i++) {for (let j = 0; j < arr.length - 1 - i; j++) {let tmp// 容易忘记写if判断语句// if判断语句容易写成arr[i] > arr[j]if (arr[j] > arr[j + 1]) {tmp = arr[j]arr[j] = arr[j + 1]arr[j + 1] = tmp}}}return arr
}
console.log(bubbleSort([2, 3, 4, 1, 2, 3, 2, 1, 9, 4, 6, 9, 6]))// 第二版(优化1)
// 第二版的优化之处在于使用一个标志位判断相邻两个数之间是否有数据交换
// 如果有数据交换,则将标志位设置为false
// 如果没有数据交换,则表明前一个数比后一个数小,不需要执行,跳出大的循环,节省时间
let bubbleSort = arr => {for (let i = 0; i < arr.length; i++) {// 设置一个标志位,默认正序排列let isTrue = truefor (let j = 0; j < arr.length - 1 - i; j++) {let tmpif (arr[j] > arr[j + 1]) {tmp = arr[j]arr[j] = arr[j + 1]arr[j + 1] = tmp// 如果交换元素,将标志位设置为false,表明是乱序排列isTrue = false}}// 这个地方容易错写成isTrue = true// 如果没有交换元素表明是正序排列,跳出大循环if (isTrue) {break}}return arr
}
console.log(bubbleSort([2, 3, 4, 1, 2, 3, 2, 1, 9, 4, 6, 9, 6]))// 第三版(优化2)
// 第三版相比较第二版的优化之处在于增加了有效长度// 假如说排序之后最后面的几个元素是已经排好序的56789,那么我们在下面的排序过程中便不需要遍历到后面的5个元素了,只需要遍历到有效长度的位置即可,而这个有效长度便是最后交换元素的索引
let bubbleSort = arr => {for (let i = 0; i < arr.length; i++) {let isTrue = true// 默认最后交换元素的索引为0let lastChangeIndex = 0// 默认初始有效长度为数组总长度let bubbleLength = arr.length - 1// 上面一行bubbleLength的初始长度容易写错//下面一行j < bubbleLength容易写错for (let j = 0; j < bubbleLength; j++) {let tmpif (arr[j] > arr[j + 1]) {tmp = arr[j]arr[j] = arr[j + 1]arr[j + 1] = tmpisTrue = false// 最后交换元素的索引lastChangeIndex = j}}// 将最后交换元素的索引赋值给有效数组长度bubbleLength = lastChangeIndexif (isTrue) {break}}return arr
}
console.log(bubbleSort([2, 3, 4, 1, 2, 3, 2, 1, 9, 4, 6, 9, 6]))

2. 数组中的第K个最大元素

// 方法一:使用sort
export default (arr, k) => {// arr.sort((a, b) => b - a)返回的是一个数组,选取数组的下标的k-1个元素,因为索引下标是从0开始的return arr.sort((a, b) => b - a)[k - 1]
}
// 方法二:面试官肯定会让你使用优化的方法来解决的,这个该怎么解决呢?
// 第一种方法使用sort()是按照顺序将整个数组全部循环一遍,这个样子会使得时间复杂度和空间复杂度都比较大
// 而优化的方法如下:
// 因为我们要求的是第K个元素的最大值,所以我们使用冒泡排序,直接直接冒泡到第K个元素即可,这样时间复杂度和空间复杂度都会节省很多
let findK = (arr, k) => {for (let i = 0; i < k + 1; i ++) {for (let j = 0; j < arr.length - 1 - i; j ++) {let tmpif (arr[j] > arr[j + 1]) {tmp = arr[j]arr[j] = arr[j + 1]arr[j + 1] = tmp}}}return arr[arr.length - k]
}
console.log(findK([1,4,5,2,7,9,2,3,7,6], 6))

3. 最大间距

// 第一版
function selectionSort(arr, k) {let max = 0let spacefor (let i = 0; i < arr.length; i++) {for (let j = 0; j < arr.length - 1 - i; j++) {if (arr[j] > arr[j + 1]) {let tmp = arr[j]arr[j] = arr[j + 1]arr[j + 1] = tmp}}// 从第二轮循环开始,因为第一轮只有一个排序完毕// 已经排好序的两个元素的差值if (i > 0) {space = arr[arr.length - i] - arr[arr.length - 1 - i]if (space > max) {max = space}}}return max
}
console.log(selectionSort([6, 4, 2, 9, 5, 3], 2))

2. 选择排序

// 第一版
function selectionSort(arr) {let len = arr.lengthfor (let i = 0; i < len; i++) {// 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。let minIndex = ifor (let j = i + 1; j < len; j++) {if (arr[j] < arr[minIndex]) { // 寻找最小的数minIndex = j // 保存最小数的索引}}// 将最小数与第一个数交换,放在起始位置(已排序元素的最后,未排序元素的最前面)let tmp = arr[i]// 容易将minIndex写成arr[i] = arr[minIndex]arr[minIndex] = tmp}return arr;
}
console.log(selectionSort([2, 3, 4, 1, 2, 3, 2, 1, 9, 4, 6, 9, 6]))

3. 奇偶排序

// 第一版
function selectionSort(arr) {let even = 1let odd = 0let r = []for (let i = 0; i < arr.length; i++) {if (arr[i] % 2 === 0) {r[odd] = arr[i]odd += 2} else {r[even] = arr[i]even += 2}}return r
}
console.log(selectionSort([4, 2, 5, 7]))

4. 选择排序


基础版本

// 方法一:基础算法
export default (arr) => {let quickSort = (arr) => {let len = arr.length// 容易忘记写递归终止的条件if (len < 2) {return arr} else {// 选标尺元素let flag = arr[0]let left = []let right = []// 把剩余的元素遍历下,比标尺元素小的放左边,大的放右边for (let i = 1, tmp; i < len; i++) {tmp = arr[i]if (tmp < flag) {left.push(tmp)} else {right.push(tmp)}}// 进行递归操作(左边数组+标尺元素+右边元素)return quickSort(left).concat(flag, quickSort(right))}}return quickSort(arr)
}

进阶版本

function quickSort(arr) {// 寻找中点function findCenter(arr, left, right) {let idx = left + 1// 小于等于for (let i = idx; i <= right; i++) {if (arr[left] > arr[i]) {swap(arr, idx, i)idx++ }}//idx - 1容易写错swap(arr, left, idx - 1)return idx - 1}// 交换两个数function swap(arr, left, right) {let tmp = arr[left]arr[left] = arr[right]arr[right] = tmp }// 递归调用function sort(arr, left, right) {// 容易忘记写这个if的判断语句if (left < right) {// 容易将这一句话写在if语句上面 let center = findCenter(arr, left, right)sort(arr, left, center - 1)sort(arr, center + 1, right)}}// 调用执行该函数sort(arr, 0, arr.length - 1)return arr}

5. 插入排序

插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法 [1] 。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动 [2] 。

时间复杂度O(n*2)
空间复杂度O(1)

 function insertSort(arr) {let len = arr.lengthlet preIndex,current// 像玩纸牌那样插入for (let i = 1; i < len; i++) {preIndex = i - 1// 一定要将arr[i]赋值给current,否则结果不如人意current = arr[i]// 这部分容易将current写成arr[i],既然前面将arr[i]赋值给current,那么后面的arr[i]就没用了,要用currentwhile (preIndex > 0 && arr[preIndex] > current) {arr[preIndex + 1] = arr[preIndex]preIndex--}arr[preIndex + 1] = current}return arr}

6. 希尔排序

7. 并归排序

function mergeSort(arr) {let len = arr.length // 递归终止的条件if (len < 2) return arr let middle = Math.floor(len / 2)// 拆分两个子数组let left = arr.slice(0, middle)let right = arr.slice(middle) // 递归拆分let mergeSortLeft = mergeSort(left)let mergeSortRight = mergeSort(right)// 合并两个子数组return merge(mergeSortLeft, mergeSortRight)}function merge(left, right) {let res = []while(left.length && right.length) {// 注意: 判断的条件是小于或等于,如果只是小于,那么排序将不稳定.if (left[0] <= right[0]) {//每次都要删除left或者right的第一个元素,将其加入result中res.push(left.shift())} else {res.push(right.shift())}}//将剩下的元素加上while(left.length) {res.push(left.shift())}while(right.length) {res.push(right.shift())}return res}console.log(mergeSort([2,4,7,5,0,1,6,9]))

8.堆排序

 // 排序的过程function sort(arr) {let n = arr.length if (n < 2) return arr // 一次整的构建最大堆的过程// i = Math.floor(n / 2)是最后一个父节点的索引,从最后一个父节点开始倒叙构建最大堆for(let i = Math.floor(n / 2); i>= 0; i--) {maxHeap(arr, i, n)}// n个元素构建最大堆for(let j = 0; j < n; j++) {// 第一个元素与最后一个元素交换 n - 1 - j是有效长度(n从0-n-1的)swap(arr, 0, n - 1 - j)// 交换一个,扔掉最后一个元素,有效长度要小一// 之所以用0交换是因为索引为0的值与最后一个值交换之后,最大堆从0的地方被破坏了,所以从0开始交换maxHeap(arr, 0, n - 1 - j - 1)}return arr}// 两个数据的交换function swap(arr, i, j) {let tmp = arr[i]arr[i] = arr[j]arr[j] = tmp }// 构建一个最大堆的过程(i是(每个节点)父元素的索引,size是有效长度)function maxHeap(arr, i, size) {// i 是父节点索引,l是左子节点的索引let l = i * 2 + 1// i 是父节点索引,r是右子节点的索引let r = i * 2 + 2// 使largest指向左节点右节点父节点中的最大值// 默认largest指向父节点let largest = i// 每次构建一次最大堆,第一个与最后一个节点互换位置,那么下次构建最大堆的时候,最后一个节点便不参与其中,所以有效长度要减少一个,数组的长度是不变的,变化的是参与构建最大堆的有效长度if (l <= size && arr[largest] < arr[l]) largest = lif (r <= size && arr[largest] < arr[r]) largest = r// 如果largest 与 i 不相同,那么将值做交换使得父节点为最大值if (largest !== i) {swap(arr, i, largest) //  交换完成之后可能会影响到子节点的最大堆的顺序,所以对子节点继续递归做构建最大堆// 交换完成之后,largest相当于子节点中的父节点,有效长度不变,因为我们没有将顶元素与最后一个元素交换来将最后一个元素从堆中移出maxHeap(arr, largest, size)}}console.log(sort([3,2,4,1,7,5,9]))
function shellSort(arr) {let len = arr.length let gap = Math.floor(len / 2)while (gap !== 0) {// 下面完全就是插入排序,只是插入排序的起始值1变为gap了for (let i = gap; i < len; i++) {let preIndex = i - gap let current = arr[i]//preIndex = i - gap >= 0-->i >= gapwhile(i >= gap && arr[preIndex] > current) {arr[preIndex + gap] = arr[preIndex]// 这个地方容易错写成 preIndex - gappreIndex -= gap }arr[preIndex + gap] = current}gap = Math.floor(gap / 2)}return arr}console.log(shellSort([2,4,7,5,0,1,6,9]))

手写js面试题

1. 手写Promise

分步骤参考链接

const PENDING = 1const FULFILLED = 2const REJECTED = 3class JPromise {constructor(excutor) {const that = this that.status = PENDING that.value = null that.error = null that.resolveCallback = []that.rejectCallback = []function resolve(value) {if (value instanceof JPromise) {value.then(resolve,reject)}if (that.status === PENDING) {that.status = FULFILLEDthat.value = valuethat.resolveCallback.forEach(fn => fn(value))}}function reject(error) {if (error instanceof JPromise) {error.then(resolve,reject)}if (that.status === PENDING) {that.status = REJECTEDthat.error = errorthat.rejectCallback.forEach(fn => fn(error))}}try {excutor(resolve, reject)} catch(e) {reject(e)}}then(onResolved, onRejected) {let that = thisif (that.status === PENDING) {return new JPromise((resolve, reject) => {setTimeout(() => {that.resolveCallback.push(() => {try {resolvePromise(onResolved(that.value), resolve, reject)} catch(e) {reject(e)}})})that.rejectCallback.push(() => {try {resolvePromise(onRejected(that.error), resolve, reject)} catch(e) {reject(e)}})})}if (that.status === FULFILLED) {return new JPromise((resolve, reject) => {setTimeout(() => {try {resolvePromise(onResolved(that.value), resolve, reject)} catch(e) {reject(e)}})})}if (that.status === REJECTED) {return new JPromise((resolve, reject) => {setTimeout(() => {try {resolvePromise(onRejected(that.error), resolve, reject)} catch(e) {reject(e)}})})}}catch(onRejected) {return this.then(null, onRejected)}finally(callback) {return this.then(callback, callback)}static resolve(value) {return value instanceof JPromise ? value : new JPromise(resolve => resolve(value))}static reject(error) {return error instanceof JPromise ? error : new JPromise(null, reject => {reject(error)})}static all(promises) {promises = Array.isArray(promises) ? promises : [promises]return new JPromise((resolve, reject) => {const length = promises.length let values = []let counter = 0promises.forEach((singlePromise, index) => {if (!(singlePromise instanceof JPromise)) {singlePromise = JPromise.resolve(singlePromise)}singlePromise.then(ret => {values[index] = retif (++counter === length) resolve(values)},reject)}) })}static race(promises) {promises = Array.isArray(promises) ? promises : [promises]return new JPromise((resolve, reject) => {promises.forEach(singlePromise => {if (!(singlePromise instanceof JPromise)) {singlePromise = JPromise.resolve(singlePromise)}singlePromise.then(resolve, reject)})})}}function resolvePromise(retValue, resolve, reject) {if (retValue instanceof JPromise) {if (retVaule.status === PENDING) {retVaule.then(ret => {resolvePromise(ret, resolve, reject)})} else {retVaule.then(resolve, reject)}} else {resolve(retValue)}}let p1 = new JPromise((resolve, reject) => {setTimeout(() => {resolve(1)},2000)})let p2 = new JPromise((resolve, reject) => {setTimeout(() => {resolve(2)},1000)})let p3 = new JPromise((resolve, reject) => {setTimeout(() => {reject('error')},2000)})let p4 = new JPromise((resolve, reject) => {setTimeout(() => {resolve(5)},1000)})JPromise.race([p1,p3,p4,]).then(res => {console.log(res)}, err => {console.log(err)})

2. 手写一个简单的双向绑定(Object.defineProperty)

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>双向绑定</title>
</head>
<body><input type="text" id="model"><div id="modelText"></div><button id="btn"></button>
</body>
<script>let input = document.querySelector('#model')let text = document.querySelector('#modelText')let btn = document.querySelector('#btn')let data = {value: ''}// 三个参数:操作的对象,改变的对象的属性,执行的逻辑// 数据-->视图  model--> viewObject.defineProperty(data, 'value', {get: function() {return 'input.value'},// 是赋值的过程,将值展示到我们想要展示在页面中的位置set: function(val) {input.value = valtext.innerText = valok.innerText = val}})// 触发的事件,触发该事件,将我们改变的值赋值给data中的value属性// 视图-->数据  view-->modelinput.addEventListener('keyup',function(val){data.value = input.value;})// 点击按钮,清空div中的内容   btn.onclick = function() {text.innerText = ''}
</script>
</body>
</html>

3. 手写一个简单的双向绑定(Proxy)

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>双向绑定</title>
</head>
<body><input type="text" id="input"><span id="p"></span>
</body>
<script>
let input = document.getElementById('input')let span = document.getElementById('p')let obj = {}// 数据  到  视图const newObj = new Proxy(obj, {get: function(target, key, receiver) {return Reflect.get(target, key, receiver)},// 容易忘记这四个参数// target目标对象,对应obj// key 对应obj的key,此处为text// value 对应obj的value,此处为 输入的值// receiver 对应接收者,此处对应为Proxy{}对象set: function(target, key, value, receiver) {input.value = value span.innerText = value//可以打印出来看他们分别是什么// console.log('target',target)// console.log('key',key)// console.log('value',value)// console.log('receiver',receiver)return Reflect.set(target, key, value, receiver)}})// 视图  到  数据input.onkeyup = function() {newObj.text = input.value}
</script>
</body>
</html>

4. 实现一个sleep函数

// Promise
const sleep = time => {return new Promise(resolve => setTimeout(resolve, time))
}
sleep(1000).then(() => {console.log(1)
})// generator
function* sleep(time) {yield new Promise(resolve => setTimeout(resolve, time))
}
sleep(1000).next().value.then(() => {console.log(1)
})// async
const sleep = time => {return new Promise(resolve => setTimeout(resolve, time))
}async function output() {let out = await sleep(1000)console.log(1)return out
}
output()// ES5
function sleep(fn, time) {if (typeof fn === 'function') {setTimeout(fn, 1000)}
}function output() {console.log(1)
}
sleep(output, 1000)

5. 实现(5).add(3).minus(2)

Number.prototype.add = function(n) {return this.valueOf() + n
}
Number.prototype.minus = function(n) {return this.valueOf() - n
}
console.log((5).add(3).minus(2))

6. 手写观察者模式

const Subject = (() => {const observers = []const addOb = (ob) => {observers.push(ob)}const notify = () => {for (let ob of observers) {if (typeof ob.update === 'function') {ob.update()}}}return {addOb, notify}})()let subA = {update: () => {console.log('subA')}}let subB = {update: () => {console.log('subB')}}Subject.addOb(subA) //添加观察者subASubject.addOb(subB) //添加观察者subBSubject.notify() //通知所有的观察者

7. 手写发布订阅模式

const PubSub = (() => {const topics = {} // 保存订阅主题//订阅某类型的主题const subscribe = (type, fn) => {if (!topics[type]) topics[type] = []topics[type].push(fn)}// 发布某类型的主题const publish = type => {if (!topics[type]) return for (let fn of topics[type]) {fn()}}return {subscribe, publish}})()let subA = {type: '1'}let subB = {type: '2'}let subC = {type: '3'};PubSub.subscribe(subA.type, () => {console.log(`A:${subA.type}`)})PubSub.subscribe(subB.type, () => {console.log(`B:${subB.type}`)})PubSub.subscribe(subC.type, () => {console.log(`C:${subC.type}`)})PubSub.publish(subB.type)

8. 实现Promise.prototype.finally

Promise.prototype.finally = (callback) => {return this.then(value => this.constructor.resolve(callback).then(() => value),err => this.constructor.resolve(callback).then(() => {throw err}))}

9. 手写Ajax

// 封装ajax
function ajax({url,type,data
}) {// 创建异步请求对象let xhr = XMLHttpRequest ? new XMLHttpRequest() : new window.ActiveXObject('Microsoft.XMLHTTP')// 处理get请求if (type.toLowerCase() === 'get' && data !== undefined) {url += "?" + dataxhr.open(type, url, true)xhr.send()}// 处理post请求if (type.toLowerCase() === 'post') {xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')xhr.open(type, url, true)data !== undefined ? xhr.send(data) : xhr.send(null)}// 绑定监听事件xhr.onreadystatechange = function() {if (xhr.readyState === 4 && xhr.status === 200) {let res = xhr.responseTextif (typeof res === 'string') {res = JSON.parse(res)}} else {console.log(xhr.responseText)}}
}

10. Promise封装ajax

function ajax({type, url, data}) {return new Promise((resolve, reject) => {let xhr = XMLHttpRequest ? new XMLHttpRequest() : new window.ActiveXObject('Microsoft.XMLHTTP')if (type.toLowerCase() === 'get' && data) {url += '?' + dataxhr.open(type, url, true)xhr.send()}if (type.toLowerCase() === 'post') {xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')xhr.open(type, url, true)data ? xhr.send(data) : xhr.send(null)}if (xhr.onreadyState === 4 && xhr.status === 200) {resovle(xhr.responseText)} else {reject('error')}})            }ajax(type, url, data).then(res => {console.log(res)}).catch(e => {throw error(e)})

11. 手写百度搜索框

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title><style>fieldset, img, input, button {border: none;padding: 0;margin: 0;outline-style: none;}ul, ol {list-style: none;margin: 0px;padding: 0px;}#box {width: 405px;margin: 200px auto;position: relative;}#txtSearch {float: left;width: 300px;height: 32px;padding-left: 4px;border: 1px solid #b6b6b6;border-right: 0;}#btnSearch {float: left;width: 100px;height: 34px;font: 400 14px/34px "microsoft yahei";color: white;background: #3385ff;cursor: pointer;}#btnSearch:hover {background: #317ef3;}#pop {width: 303px;border: 1px solid #ccc;padding: 0px;position: absolute;top: 34px;}#pop ul li {padding-left: 5px;}#pop ul li:hover {background-color: #CCC;}</style>
</head>
<body>
<div id="box"><input type="text" id="txtSearch"><input type="button" value="百度一下" id="btnSearch">
</div>
<script>var box = document.getElementById("box");var txt = document.getElementById("txtSearch");//模拟从后台获取到的数据var data = ["a", "abc", "abbbb", "abxxxx", "xyz", "abcdef", "abzzzz",'中国','中央'];//1.用户输入的同时 从datas中 寻找符合要求的 项目 放到一个新数组中txt.onkeyup = function() {var val = this.value;//获取当前文本框中的内容//1.1从datas中 寻找符合要求的 项目 放到一个新数组中var arr = [];//1.2遍历datas里面的每一项 判断是否符合要求 如果符合要求 就放到新数组中for (var i = 0; i < data.length; i++) {//原数组的每一项数据//判断每一个data是否以当前的val开头if (data[i].indexOf(val) === 0) {//符合要求arr.push(data[i]);}}var popDiv = document.getElementById("pop");if (popDiv) {//如果进来了 说明已经有了 就要把他干掉box.removeChild(popDiv);}// 如果后台中没有对应的数据,不创建DOMif (arr.length === 0) {return;}// 如果输入内容为空,不创建DOMif (val === "") {return;}// 创建一个div,设置div的id,将div加到box中var popDiv = document.createElement('div')popDiv.id = 'pop'box.appendChild(popDiv)// 创建一个ul,将ul加到div中let ul = document.createElement('ul')popDiv.appendChild(ul)// 遍历符合规则的数据,每遍历一个数据动态创建一个li,将数据的内容添加到li中for (let i = 0; i < arr.length; i++) {let li = document.createElement('li')ul.appendChild(li)li.innerText = arr[i]// 关键词高亮显示//split(val):将字符串以val为分隔符,转换为数组,例如:‘abcd’.split('a')为['','bcd'].//join(val):将数组以val为分隔符转换为字符串,例如['','bcd'].join('a') 为‘abcd’,例如['b','c','d'].join('a')为‘abcad’li.innerHTML = li.innerHTML.split(val).join('<span style="color:red;">' + val + '</span> ')// 点击li,将li中的内容添加到input中,将pop隐藏li.onclick = function() {txt.value = this.innerTextpopDiv.style.display = "none"}// 鼠标移入高亮显示li.onmousemove = function() {this.style.backgroundColor = "red"}li.onmouseout = function() {this.style.backgroundColor = ""}// 点击空白,pop隐藏document.onclick = function() {popDiv.style.display = "none"}}}
// 页面跳转btn.onclick = function() {window.location.href = 'https://www.baidu.com'}//todo:用上下键选择内容(涉及到事件对象)(有兴趣可以研究一下)</script>
</body>
</html>

12. 原生js实现耳机下拉框联动

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><div id='container'><select id="line" onchange="getStation()"><option value="0">请选择</option></select><select id="station"><option value="0">请选择</option></select></div>
</head>
<body><script>let lines = ['102','103']let stationName = [["百万庄西口", "百万庄中街", "百万庄东口", "展览路", "阜成门外", "阜成门", "阜成门内", "白塔寺", "西四路口东", "西安门", "北海", "故宫", "沙滩路口西", "美术馆东", "东四路口东", "朝内小街", "朝阳门内", "朝阳门外", "神路街", "东大桥路口东", "关东店", "呼家楼西", "小庄路口东", "红庙路口西", "红庙路口东"],             ["动物园枢纽站", "二里沟", "百万庄", "甘家口大厦", "甘家口东", "阜外西口", "展览路", "阜成门外", "阜成门", "阜成门内", "白塔寺", "西四路口西", "缸瓦市", "甘石桥", "西单商场", "西单路口南", "宣武门内", "校场口", "菜市口北", "果子巷", "虎坊桥路口南", "太平街北口", "太平街", "陶然桥北", "永定门长途汽车站", "北京南站"]         ]let line = document.getElementById('line')// 将数据渲染到第一个下拉框中function aa() {for(let i = 0; i < lines.length; i++) {line.innerHTML += `<option>${lines[i]}</option>`}}aa()// 实现二级下拉框等级联动并将联动的内容渲染到二级下拉框的下面function getStation() {let station = document.getElementById('station')// 筛选出一级下拉框中选中的标签索引中对应的stationName数组中的内容筛// selectedIndex容易写成selectIndexlet stations = stationName[line.selectedIndex - 1]for (let j = 0; j < stations.length; j++) {// 如果直接写成station[j],那么会将下拉框中的第一个元素无法展示出来// 容易错写成station[j + 1].setOption()station[j + 1] = new Option(stations[j],stations[j])}}</script>
</body>
</html>

13. 手写防抖和节流

13.1 防抖

let p = document.getElementById('p')function debounce(fn, wait) {let timeout return function() {let that = this let arg = arguments clearTimeout(timeout) timeout = setTimeout(function() {fn.apply(that, arg)}, wait)}}p.onmousemove = debounce(func,1000)function func() {console.log(1)}

13.2 节流

使用时间戳

 let p = document.getElementById('p')function throttle(fn, wait) {let timeout, pre = 0return function() {let that = this let args = arguments // new Date()这种形式是时间格式字符,不是数字let now = +new Date()// let now = new Date().getTimeout()if (now - pre > wait) {fn.apply(that, args)pre = now}}}p.onmousemove = throttle(func,1000)function func() {console.log(1)}

使用定时器

let p = document.getElementById('p')function throttle(fn, wait) {let timeoutreturn function() {let that = this let args = arguments if(!timeout) {timeout = setTimeout(function() {timeout = nullfn.apply(that, args)},1000)}}}p.onmousemove = throttle(func,1000)function func() {console.log(1)}

使用定时器实现和时间戳一样的效果

function throttle(fn, wait) {let timeoutreturn function() {let that = this let args = arguments if(!timeout) {fn.apply(that, args)timeout = setTimeout(function() {timeout = null},1000)}}}

14. 手写深拷贝

//乞丐版// JSON.parse(JSON.stringify())// 升级版function clone(target, map = new WeakMap()) { //WeakMap替换Map,实现不用的内存,系统自动收回//克隆基本类型的值if (!isObject(target)) {return target } else {// 克隆数组和对象let cloneTarget = Array.isArray(target) ? [] : {}// 解决循环引用的问题if (map.get(target)) return map.get(target)map.set(target, cloneTarget)// 克隆数组和对象for (let key in target) {if (isObject(target[key])) {cloneTarget[key] = clone(target[key], map)} else {cloneTarget[key] = target[key]}}// 克隆Symbollet symbols = Object.getOwnPropertySymbols(target)if (symbols.length) {symbols.forEach(symbol => {if (isObject(target[symbol])) {cloneTarget[symbol] = clone(target[symbol], map)} else {cloneTarget[symbol] = target[symbol]}})}// 克隆函数if (typeof target === 'function') return targetreturn cloneTarget}} // 判断是否是objectfunction isObject(target) {let type = typeof target return type === 'object' && type !== null }

15. 手写懒加载

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><div id='container'><img src="data:images/loading.gif" data-src="data:images/1.png"><img src="data:images/loading.gif" data-src="data:images/2.png"><img src="data:images/loading.gif" data-src="data:images/3.png"><img src="data:images/loading.gif" data-src="data:images/4.png"><img src="data:images/loading.gif" data-src="data:images/5.png"><img src="data:images/loading.gif" data-src="data:images/6.png"><img src="data:images/loading.gif" data-src="data:images/7.png"><img src="data:images/loading.gif" data-src="data:images/8.png"><img src="data:images/loading.gif" data-src="data:images/9.png"><img src="data:images/loading.gif" data-src="data:images/10.png"></div>
</head>
<body><script>function lazyload() {let images = document.getElementsByTagName('img')let len = images.length//存储图片加载到的位置,避免每次都从第一张图片开始遍历 let n = 0 return function() {// 浏览器可视区域宽度let seeHeight = document.documentElement.clientHeight// 滚动条滚动的宽度(document.body.scrollTop是谷歌浏览器中的滚动条滚动的宽度)let scrollTop = document.documentElement.scrollTop || document.body.scrollTopfor (let i = n; i < len; i++) {//images[i].offsetTop代表图片距离上方或者上层控件的位置if (images[i].offsetTop < seeHeight + scrollTop) {if (images[i].getAttribute('src') === 'images/loading.gif') {images[i].src = images[i].getAttribute('data-src')}n++}}}}//防抖函数绑定在 scroll 事件上,当页面滚动时,避免函数被高频触发,function throttle(fn, wait) {let timeout return function() {let that = this let args = arguments timeout = setTimeout(function() {fn.apply(that, args)}, wait)}}window.addEventListener('scroll', throttle(lazyload, 1000))</script>
</body>
</html>

16. 实现一个new

function myNew() {// 1. 获取构造函数// 构造函数是伪数组的第一个参数,由于是伪数组,所以不能直接调用shift()方法,用call来调用数组上的shift方法var constr = Array.prototype.shift.call(arguments);
//   2. 创建一个空对象var obj = {}// 3. 将新建对象的隐式原型指向构造函数的显示原型obj.__proto__ = constr.prototype
//   4. 执行构造函数中的代码var result = constr.apply(obj, arguments);
//   5. 如果函数是Object类型的值则返回,否则返回新对象return result instanceof Object? result : obj;
}
function Person(name, age, job) {this.name = name;this.age = age;this.job = job;this.sayName = function() {console.log(this.name)}
}
// var person = new Person('Nicholas', 29, 'Front-end developer');
var person = myNew(Person, 'Nicholas', 29, 'Front-end developer');
console.log(person.name) // Nicholas
person.sayName();        // Nicholas
console.log(person.__proto__ === Person.prototype);

17. 手写call、apply、bind

apply

Function.prototype.myCall = function(context) {// 1.判断有没有传入要绑定的对象,没有默认是window,如果是基本类型的话通过Object()方法进行转换(解决问题3)var context = Object(context) || window /*在指向的对象obj上新建一个fn属性,值为this,也就是fn()相当于obj变成了{value: 'hdove',fn: function fn() {console.log(this.value);}}*/context.fn = this // 2.保存返回值let result = ''// 3.取出传递的参数(从下标为0开始截取到最后) 第一个参数是this对象(在本例中是{value:li}), 下面是三种截取除第一个参数之外剩余参数的方法(解决问题1)// 首先将类数组的对象arguments转换为数组,再采用数组的方法进行截取(返回值是一个数组)const args = [...arguments].slice(1)// const args = Array.prototype.slice.call(arguments, 1)// const args = Array.from(arguments).slice(1)// 4.执行这个方法,并传入参数 ...是es6的语法用来展开数组result = context.fn(...args)//5.删除该属性(解决问题4)delete context.fn //6.返回 (解决问题2)return result}//测试用例const obj = {value: 'li'}function fn(name, age) {return {value: this.value,age,name,}}console.log(fn.myCall(obj,'lz',26))

call

Function.prototype.myCall = function(context,args) {var context = Object(context) || window context.fn = this let result = ''//4. 判断有没有传入argsif (args) {result = context.fn(...args)} else {result = context.fn()}delete context.fn return result}//测试用例const obj = {value: 'li'}function fn(name, age) {return {value: this.value,age,name,}}console.log(fn.myCall(obj,['lz',26]))

bind

Function.prototype.myBind = function(context) {if (typeof this !== 'function') {throw Error('Not function')}let that = this let args1 = [...arguments].slice(1)const bindFn =  function() {let args2 = [...arguments]that.apply(this instanceof bindFn ? this : context, [...args1, ...args2])}function Func() {}Func.prototype = this.prototypebindFn.prototype = new Func()return bindFn}const obj = {value: 'll'}function fn(name, age) {this.test = 'test'console.log(this.value)console.log(name)console.log(age)}fn.prototype.pro = 'pro'let bindFn = fn.myBind(obj, 'lz')// bindFn(26)let newBind = new bindFn()newBind.__proto__.pro = 'p'console.log(newBind.pro)console.log(fn.prototype.pro)

18. 手写函数柯里化

Function.prototype.myBind = function(context) {if (typeof this !== 'function') {throw Error('Not function')}let that = this let args1 = [...arguments].slice(1)const bindFn =  function() {let args2 = [...arguments]that.apply(this instanceof bindFn ? this : context, [...args1, ...args2])}function Func() {}Func.prototype = this.prototypebindFn.prototype = new Func()return bindFn}const obj = {value: 'll'}function fn(name, age) {this.test = 'test'console.log(this.value)console.log(name)console.log(age)}fn.prototype.pro = 'pro'let bindFn = fn.myBind(obj, 'lz')// bindFn(26)let newBind = new bindFn()newBind.__proto__.pro = 'p'console.log(newBind.pro)console.log(fn.prototype.pro)

19. 手写无缝轮播

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>手写无缝轮播图</title><style>* {padding: 0;margin: 0;list-style: none;border: 0;}.all {width: 500px;height: 200px;padding: 7px;border: 1px solid #ccc;margin: 100px auto;position: relative;}.screen {width: 500px;height: 200px;overflow: hidden;position: relative;}.screen li {width: 500px;height: 200px;overflow: hidden;float: left;}.screen ul {position: absolute;left: 0;top: 0px;width: 3000px;}.all ol {position: absolute;right: 10px;bottom: 10px;line-height: 20px;text-align: center;}.all ol li {float: left;width: 20px;height: 20px;background: #fff;border: 1px solid #ccc;margin-left: 10px;cursor: pointer;}.all ol li.current {background: yellow;}#arr {display: none;}#arr span {width: 40px;height: 40px;position: absolute;left: 5px;top: 50%;margin-top: -20px;background: #000;cursor: pointer;line-height: 40px;text-align: center;font-weight: bold;font-family: '黑体';font-size: 30px;color: #fff;opacity: 0.3;border: 1px solid #fff;}#arr #right {right: 5px;left: auto;}</style>
</head>
<body><div  ><div ><ul><li><img src="https://www.baidu.com/img/flexible/logo/pc/result.png" width="500" height="200" style="background-color: yellowgreen;"/></li><li><img src="https://www.baidu.com/img/flexible/logo/pc/result.png" width="500" height="200" style="background-color: pink;"/></li><li><img src="https://www.baidu.com/img/flexible/logo/pc/result.png" width="500" height="200" style="background-color: skyblue;"/></li><li><img src="https://www.baidu.com/img/flexible/logo/pc/result.png" width="500" height="200" style="background-color: greenyellow;"/></li><li><img src="https://www.baidu.com/img/flexible/logo/pc/result.png" width="500" height="200" style="background-color: plum;"/></li><li><img src="https://www.baidu.com/img/flexible/logo/pc/result.png" width="500" height="200" style="background-color: orange;"/></li></ul><ol><li >1</li><li>2</li><li>3</li><li>4</li><li>5</li></ol></div><div ><span ><</span><span >></span></div></div><script>// 1.获取页面对应的元素var box=document.getElementById("box"); //最外部大盒子var arr=document.getElementById("arr");var screen=document.getElementsByClassName("screen")[0]; //轮播图显示区域divvar ul=document.getElementsByTagName("ul")[0]; //显示图片的ulvar ol=document.getElementsByTagName("ol")[0]; //显示页码的olvar left=document.getElementById("left"); //上一张箭头var right=document.getElementById("right"); //下一张箭头var index=0; 声明一个变量记录图片的索引,默认第0张图片//2.给box添加鼠标移入和移出事件//2.1 鼠标移入box.onmouseover= function () {arr.style.display="block"; //显示上一页下一页箭头clearInterval(timeId); //清除定时器(即鼠标移入时,图片要停止自动轮播)};//2.2 鼠标移出box.onmouseout= function () {arr.style.display="none"; //隐藏箭头timeId=setInterval(scroll,2000);  //重启定时器(鼠标移出,图片要恢复自动轮播)};//3.给上一页下一页箭头添加点击事件//3.1 下一页,图片向左轮播right.onclick= function () {scroll();};//3.2 上一页,图片向右轮播left.onclick= function () {//(1)边界检测,如果当前已经是第一张,则不做任何处理if(index==0){//无限轮播原理:如果当前是第一张,则偷偷修改ul的位置是最后一张(第一张与最后一张是同一张图片)index=ul.children.length-1; //index恢复到最后一张ul.style.left=-index*screen.offsetWidth+"px"; ul回到最后一张位置}//(2)索引自减index--;// (3)向左移动ul:目标距离 = -screen的宽度 * 索引animationMove(ul,-index*screen.offsetWidth,10);indexShow(); //同步页码样式};//4.给页码添加点击事件for(var i=0;i<ol.children.length;i++){//4.1 循环遍历数组时给每一个页码添加一个liIndex属性记录下标ol.children[i].liIndex=i;ol.children[i].onclick= function () {index=this.liIndex-1;scroll();};}var timeId=setInterval(scroll,2000);// 封装一个向右轮播的函数function scroll(){//(1)边界检测:如果当前已经是最后一张(第n+1张,n代表需要轮播的图片数量)if(index==ul.children.length-1){//无限轮播的原理就是滚动到最后一张的时候,偷偷快速的改变ul的位置到第一张(不要任何动画,一瞬间改变)index=0; //index恢复到0ul.style.left=0+"px"; //ul回到初始位置}// (2)索引自增index++;// (3)向右移动ul:目标距离 = -screen的宽度 * 索引animationMove(ul,-index*screen.offsetWidth,10);indexShow(); //同步页码样式}//5.页码样式保持同步:排他思想(当前页码添加样式,其他页码移除该样式)function indexShow(){for(var i=0;i<ol.children.length;i++){if(i==index){ol.children[i].classList.add("current");}else{ol.children[i].classList.remove("current");}//特殊情况:当index为最后一张的时候,页码应该显示第一张if(index==ul.children.length-1){ol.children[0].classList.add("current");}}}// 封装一个滚动动画函数function animationMove(obj,target,speed){clearInterval(obj.timeId);  //每次执行动画先清除原有的定时器obj.timeId=setInterval(function () {var currentLeft=obj.offsetLeft; //获取当前位置var isLeft=currentLeft>target?true:false;   //是否往左走if(isLeft){currentLeft-=10;    //往左走}else{currentLeft+=10;    //往右走}if(isLeft?currentLeft>target:currentLeft<target){obj.style.left=currentLeft+"px";  //如果当前位置不是在目标位置则进行位置处理}else{clearInterval(obj.timeId);obj.style.left=target+"px";}// if(currentLeft>target){//     currentLeft-=10;//     obj.style.left=currentLeft+"px";// }else if(currentLeft<target){//     currentLeft+=10;//     obj.style.left=currentLeft+"px";// }else{//     clearInterval(obj.timeId);//     obj.style.left=target+"px";// }},speed);}</script>
</body>
</html>

20. 手写实现一个事件委托

function delegate(element, eventType, selector, fn) {element.addEventListener(eventType, e => {let el = e.targetwhile (!el.matches(selector)) {if (element === el) {el = nullbreak}el = el.parentNode}el && fn.call(el, e, el)},true)return element}

21. 手写一个可以拖拽的div

var dragging = false
var position = nullxxx.addEventListener('mousedown',function(e){dragging = trueposition = [e.clientX, e.clientY]
})document.addEventListener('mousemove', function(e){if(dragging === false) return nullconst x = e.clientXconst y = e.clientYconst deltaX = x - position[0]const deltaY = y - position[1]const left = parseInt(xxx.style.left || 0)const top = parseInt(xxx.style.top || 0)xxx.style.left = left + deltaX + 'px'xxx.style.top = top + deltaY + 'px'position = [x, y]
})
document.addEventListener('mouseup', function(e){dragging = false
})

22.数组去重




23. 实现flat

let flatDeep = (arr) => {return arr.reduce((res, cur) => {if(Array.isArray(cur)){return [...res, ...flatDep(cur)]}else{return [...res, cur]}},[])
}

24. 实现一个对象类型的函数

let isType = (type) => (obj) => Object.prototype.toString.call(obj) === `[object ${type}]`// let isArray = isType('Array')
// let isFunction = isType('Function')
// console.log(isArray([1,2,3]),isFunction(Map))

25. 实现instanceof

function my_instance_of(leftVaule, rightVaule) {if(typeof leftVaule !== 'object' || leftVaule === null) return false;let rightProto = rightVaule.prototype,leftProto = leftVaule.__proto__;while (true) {if (leftProto === null) {return false;}if (leftProto === rightProto) {return true;}leftProto = leftProto.__proto__}
}

26. reduce相关

1. 手写reduce

Array.prototype.myreduce = function(fn, initVal) {let result = initVal,i = 0;if(typeof initVal  === 'undefined'){result = this[i]i++;}while( i < this.length ){result = fn(result, this[i])}return result
}

2. 求和求体积

3. 计算数组中每个元素出现的次数

4. 数组去重

5. 将多维数组转为一位数组

27. 实现object.create()

//实现Object.create方法
function create(proto) {function Fn() {};Fn.prototype = proto;Fn.prototype.constructor = Fn;return new Fn();
}
let demo = {c : '123'
}
let cc = Object.create(demo)

28. 十进制转换

function Conver(number, base = 2) {let rem, res = '', digits = '0123456789ABCDEF', stack = [];while (number) {rem = number % base;stack.push(rem);number = Math.floor(number / base);}while (stack.length) {res += digits[stack.pop()].toString();}return res;
}

29. 十进制转换

给定10进制数,转换成[2~16]进制区间数

function Conver(number, base = 2) {let rem, res = '', digits = '0123456789ABCDEF', stack = [];while (number) {rem = number % base;stack.push(rem);number = Math.floor(number / base);}while (stack.length) {res += digits[stack.pop()].toString();}return res;
}

30.rgb与16进制之间的转换

1. rgb转16进制

String.prototype.colorHex = function () {// RGB颜色值的正则var reg = /^(rgb|RGB)/;var color = this;if (reg.test(color)) {var strHex = "#";// 把RGB的3个数值变成数组var colorArr = color.replace(/(?:\(|\)|rgb|RGB)*/g, "").split(",");// 转成16进制for (var i = 0; i < colorArr.length; i++) {var hex = Number(colorArr[i]).toString(16);if (hex === "0") {hex += hex;}strHex += hex;}return strHex;} else {return String(color);}
};

2. 16进制转rgb

String.prototype.colorRgb = function () {// 16进制颜色值的正则var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;// 把颜色值变成小写var color = this.toLowerCase();if (reg.test(color)) {// 如果只有三位的值,需变成六位,如:#fff => #ffffffif (color.length === 4) {var colorNew = "#";for (var i = 1; i < 4; i += 1) {colorNew += color.slice(i, i + 1).concat(color.slice(i, i + 1));}color = colorNew;}// 处理六位的颜色值,转为RGBvar colorChange = [];for (var i = 1; i < 7; i += 2) {colorChange.push(parseInt("0x" + color.slice(i, i + 2)));}return "RGB(" + colorChange.join(",") + ")";} else {return color;}
};

31. 原生js手写table表格

<!DOCTYPE HTML>
<html>
<head>
<meta charset=UTF-8>
<title>table</title>
<style>
table {margin:auto;width: 60%;border: 1px solid black;border-collapse: collapse;
}table caption {color: blue;font: 34px consolas bold;
}table th, table td {border: 1px solid black;text-align:center;
}table th {font: 20px consolas bold;background-color: gray;
}table tr:nth-child(n+2){background-color: cyan;
}table tr:nth-child(2n+2)  {background-color: red;
}
</style>
<script>
var data = [{id: "001", fullname: "张三", sex: "男", score: [98,33,48]},{id: "002", fullname: "李四", sex: "w", score: [11,22,33]},{id: "003", fullname: "kyo", sex: "m", score: [22,33,55]},{id: "004", fullname: "yamazaki", sex: "w", score: [99, 100, 80]}
];var Student = function (id, fullname, sex, score)
{this.id = id;this.fullname = fullname;this.sex = sex;this.score = score;
}var Score = function (chinese, math, english)
{this.chinese = chinese;this.math = math;this.english = english;
}var students = [];
for (var i = 0; i < data.length; i++)
{var d = data[i];var s = d["score"];students.push(new Student (d["id"], d["fullname"], d["sex"], new Score(s[0], s[1], s[2])));
}onload = function ()
{var table = document.createElement("table");var tbody = document.createElement("tbody");table.appendChild(tbody);var caption = table.createCaption();caption.innerHTML = "学生成绩表";var tr = tbody.insertRow (0);var str = "学号,姓名,性别,语文,数学,英语,嘤嘤嘤".split(",");for (var i = 0; i < str.length; i++){var th = document.createElement("th");th.innerHTML = str[i];tr.appendChild (th);}for (var i = 0; i < students.length; i++){var tr = tbody.insertRow (tbody.rows.length);var obj = students[i];for (var p in obj){var op = obj[p];if (p != "score"){var td = tr.insertCell (tr.cells.length);td.innerHTML = op;}else{for (var p in op){var td = tr.insertCell (tr.cells.length);td.innerHTML = op[p];}}}}document.body.appendChild(table);
}
</script>
<body></body>
</html>

32. JSONP 动态创建script进行跨域请求

印象笔记算法汇总

1. 手写一个自定义事件

var myEvent = new Event('clickTest');
element.addEventListener('clickTest', function () {
console.log('smyhvae');
});
//元素注册事件
element.dispatchEvent(myEvent);
//注意,参数是写事件对象 myEvent,不是写 事件名 clickTest

2. 手写一个new

当new Foo()时发生了什么:
(1)创建一个新的空对象实例,它继承自Foo.prototype。
(2)将此空对象的隐式原型指向其构造函数的显示原型。
(3)执行构造函数Foo(传入相应的参数,如果没有参数就不用传),同时 this 指向这个新实例。newFoo 等同于 new Foo(),只能用在不传递任何参数的情况。
(4)如果返回值是一个新对象,那么直接返回该对象,这个对象会取代整个new出来的结果;如果无返回值或者返回一个非对象值,那么就将步骤(1)创建的对象返回。

//Fun为构造函数, args表示传参
function myNew(Fun, ...args) {// 1.在内存中创建一个新对象let obj = {};// 2.把新对象的原型指针指向构造函数的原型属性obj.__proto__ = Fun.prototype;// 3.改变this指向,并且执行构造函数内部的代码(传参)let res = Fun.apply(obj, args);// 4.判断函数执行结果的类型if (res instanceof Object) {return res;} else {return obj;}
}let obj = myNew(One, "XiaoMing", "18");
console.log("newObj:", obj);

3. 对象继承的集中方式

3.1 借助构造函数

function Parent1() {
this.name = 'parent1 的属性';
}
function Child1() {
Parent1.call(this);         //【重要】此处用 call 或 apply 都行:改变 this 的指向
this.type = 'child1 的属性';
}
console.log(new Child1);

3.2 通过原型链实现继承

/*    通过原型链实现继承     */
function Parent() {
this.name = 'Parent 的属性';
}
function Child() {
this.type = 'Child 的属性';
}
Child.prototype = new Parent(); //【重要】
console.log(new Child());

3.3 组合的方式:构造函数 + 原型链

/*    组合方式实现继承:构造函数、原型链     */
function Parent3() {
this.name = 'Parent 的属性';
this.arr = [1, 2, 3];
}
function Child3() {
Parent3.call(this); //【重要1】执行 parent方法
this.type = 'Child 的属性';
}
Child3.prototype = new Parent3(); //【重要2】第二次执行parent方法    var child = new Child3();

3.4 组合的方式:构造函数 + 原型链 优化1

function Parent4() {
this.name = 'Parent4'
this.play = [1, 2, 3]}
function Child4() { //【重要1】
Parent4.call(this)
this.type = 'child4'
}
Child4.prototype = Parent4.prototype  //【重要2】
let s5 = new Child4()
let s6 = new Child4()
console.log(s5, s6)

3.5 组合的方式:构造函数 + 原型链 优化2

function Parent5() {
this.name = 'Parent4'
this.play = [1, 2, 3]
}
function Child5() {
Parent4.call(this)
this.type = 'child4'
}
Child5.prototype =Object.create(Parent5.prototype)
Child5.prototype.constructor = Child5
let s5 = new Child4()
let s6 = new Child4()
console.log(s5, s6)

4. 请把俩个数组 [A1, A2, B1, B2, C1, C2, D1, D2] 和 [A, B, C, D],合并为 [A1, A2, A, B1, B2, B, C1, C2, C, D1, D2, D]

5. [2, 10, 3, 4, 5, 11, 10, 11, 20],将其排列成一个新数组,例如 [[2, 3, 4, 5], [10, 11], [20]]。

// 得到一个两数之间的随机整数,包括两个数在内
function getRandomIntInclusive(min, max) {min = Math.ceil(min);max = Math.floor(max);return Math.floor(Math.random() * (max - min + 1)) + min; //含最大值,含最小值
}
// 随机生成10个整数数组, 排序, 去重
let initArr = Array.from({ length: 10 }, (v) => { return getRandomIntInclusive(0, 99) });
initArr.sort((a,b) => { return a - b });
initArr = [...(new Set(initArr))];// 放入hash表
let obj = {};
initArr.map((i) => {// 此处是为了使得大数组中有多个小数组的const intNum = Math.floor(i/10);if (!obj[intNum]) obj[intNum] = [];obj[intNum].push(i);
})// 输出结果
const resArr = [];
for(let i in obj) {resArr.push(obj[i]);
}
console.log(resArr);

6. 如何把一个字符串的大小写取反(大写变小写小写变大写),例如 ’AbC’ 变成 ‘aBc’ 。

7. 实现一个字符串匹配算法,从长度为 n 的字符串 S 中,查找是否存在字符串 T,T 的长度是 m,若存在返回所在位置。

8. 将一个排序好的数组打乱


9. 手写记忆函数



javascript算法+手写js面试题相关推荐

  1. java面笔试_java笔试手写算法面试题大全含答案

    java笔试手写算法面试题大全含答案 1.统计一篇英文文章单词个数. public class WordCounting { public static void main(String[] args ...

  2. 【每日手写JS代码】

    文章目录 一. 手写JS 1-1 数组方法 1月5号 数组扁平化 1月6号 Array.prototype.map() 1月7号 Array.prototype.filter() 1月8号 Array ...

  3. 【javascript】手写一个webpack loder

    [javascript]手写一个webpack loder 手写一个loader 为什么需要loader?  webpack 实际上只能处理js文件,那么对于除了js文件的其他类型的文件 比如 css ...

  4. 课程设计(毕业设计)—基于机器学习KNN算法手写数字识别系统—计算机专业课程设计(毕业设计)

    机器学习KNN算法手写数字识别系统 下载本文手写数字识别系统完整的代码和课设报告的链接(或者可以联系博主koukou(壹壹23七2五六98),获取源码和报告):https://download.csd ...

  5. 算法------手写LRU算法

    算法------手写LRU算法 LRU是Redis中常用的内存淘汰算法. 意思是:当缓存容量满的时候,淘汰最近很少使用的数据. 具体实现逻辑: 把缓存放到双向链表当中,最近使用过.或新放入的缓存,放到 ...

  6. 神经网络算法---手写数字体识别

    文章目录 神经网络的背景 多层向前神经网络 设计神经网络结构 交叉验方法 Backpropagation 算法 激活函数 手写数字例子 神经网络的背景 1,1980年backpropagation是神 ...

  7. KNN算法--手写识别

    文章目录 前言 一.手写识别系统 二.主要代码 1.引入库 2.分类器代码: 3.将图像转化为测试向量 4.手写数字识别系统测试代码 总结 前言 KNN算法的介绍在上篇文章中已经提及,详情请见KNN算 ...

  8. c语言用单链表实现lru算法,手写单链表实现和LRU算法模拟

    手写单链表,实现增删改查 package top.zcwfeng.java.arithmetic.lru; //单链表 public class LinkedList { Node list; int ...

  9. 【Java基础】IO流与文件操作的一些手写代码面试题

    概述 代码 概述 IO流是我们最基础的数据操作,最近我身边的一些朋友经常遇到这样的面试题,有的居然还是手写代码,真的太残酷了:不过话说回来,IO流确实是编程里最最基础的操作了,需要我们好好掌握,熟记于 ...

最新文章

  1. LCD12864示例子程序
  2. 几个常见的网络故障分析
  3. mysql 挑战握手协议_什么是挑战握手认证协议协议,在现实中有哪些应用?
  4. 对抗微软?索尼36亿美金收购游戏开发商Bungie
  5. 电脑扫描文件怎么弄_彻底清除手机垃圾文件,释放内存的方法
  6. 配置python开发环境搭建_Eclipse配置Python开发环境
  7. 【OpenCV】图像分割
  8. linux服务之FTP服务篇
  9. DoEvents 方法使用小结
  10. NVR+DVR+CVR
  11. 谷歌(chrome)恐龙小游戏外挂
  12. 分数相同的排名处理php,SQL实现相同分数排名相同--sql 语句 并列排名的问题
  13. 【vscode简单入门(三)】vscode巨实用的基础插件推荐(不定期更新)
  14. 云计算现在前景如何?怎么转型成为云计算工程师?
  15. 计算机通电后自动断电,电脑开机自动断电,详细教您电脑开机自动断电怎么解决...
  16. php simplexmlelement object 数组,php中将SimpleXMLElement Object数组转化为普通数组
  17. MATLAB中如何作随时间变化图
  18. 静态库,动态库是啥,有啥区别(静态函数库/动态函数库)
  19. php 自动关键词,php实现自动获取生成关键词功能
  20. 股票量化交易Python——计算收益率

热门文章

  1. 网关地址对于计算机的重要性
  2. 好压压缩文件目录乱码的解决
  3. 智能家居中的软件测试,智能家居测试思路
  4. python是否高送转预测股票_炒股票的四个阶段
  5. 苹果电脑macbookpro清理垃圾软件 mac系统免费清洁
  6. Spring Cloud Stream核心原理介绍
  7. 问题 H: 口袋的天空(Kruscal)
  8. 《重新定义公司》读书笔记
  9. 新库上线 | CnOpenData中国游戏审批数据
  10. waf怎么读_技术分享:杂谈如何绕过WAF(Web应用防火墙)