前置知识
链表
链表是一种通过指针串联在一起的线性结构,每个节点都由两部分组成,一个是数据域,一个是指针域(存放指向下一个节点的指针)最后一个节点的指针域指向null
链接的入口节点称为链表的头节点head
链表类型
- 单链表
-
双链表
每个节点都有2个指针域,一个指向上个节点,一个指向下个节点,既可以向前查询也可以向后查询
-
循环链表
顾名思义,就是首尾相连的链表,其中著名的有约瑟夫环问题(待定)
链表的存储方式
链表的存储方式在内存中不是连续的,它是散乱分布在内存里,通过指针域的指针来链接在内存上的各个节点。
这个链表的起点节点是2,终点节点是7,其余各个节点散乱在内存中,通过指针进行链接。
算法的时间复杂度和空间复杂度
-
时间复杂度
是指执行当前算法所消耗的时间,「 大O符号表示法 」,即 T(n) = O(f(n)) demo
for(let i=0; i<n; i++) { console.log(i); }这段执行算法的时间复杂度就是O(n) 常见的时间复杂度的例子有:
- 常数阶O(1)
无论代码执行了多少行,只要没有循环等复杂结构,那么这个代码的时间复杂度都是O(1)var i = 1; var j = 1; ++i; j++; var m = i + j- 对数阶O(logN)
var i = 1; while(i<n){ i = i*2; }在while循环里面,每次i都乘以2,i距离n的大小就越来越近,假设循环x次之后,i就大于n了,这个循环就退出,那么2的x方等于n,x=log2 (n);当n无限大的时候,底数可以忽略,因此这个代码的时间复杂度为O(logn)- 线性阶O(n)
一开始的demo中,for循环代码会执行n遍,因此它消耗的时间是随着n的变化而变化,这类代码都可用O(n)来表示它的时间复杂度- 线性对数阶O(nlogN)
就是将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是n*O(logn),也就是O(nlogN)for(m=1; m<n,m++){ i=1; while(i<n){ i =i*2; } }- 平方阶O(n²)
如果把O(n)的时间复杂度代码在循环一遍,那它的时间复杂度就是O(n^2), 还有如O(m*n)的时间复杂度,其实也是嵌套循环了。- 立方阶O(n³)
- K次方阶O(n^k)
立方阶相当于三层n循环,k次方代表k层n循环了- 指数阶(2^n)
通常情况下斐波那契的递归算法属于指数阶的时间复杂度。如下代码,a(0)=a(1)=1;同时a(n)=a(n-1)+a(n-2)+1, 当n=1时,复杂度为O(1),当n>1时,n会被一层一层拆分。指数级别的增长,所以时间复杂度O(2^n)function a(n) { if(n<=1){ return 1; } return a(n-1)+a(n-2) }O(1) < O(log n) < O(√n) < O(n) < O(nlog n) < O(n^2) < O(2^n) < O(n!)
-
空间复杂度
既然时间复杂度用来计算程序具体耗时的,那么空间复杂度就是程序具体占用的空间。 常用的也是有O(1),O(n), O(n^2)
- 空间复杂度O(1)
如果算法的临时空间不会随着某个变量n的变化而变化,那么它的算法空间复杂度为一个常量,O(1)- 空间复杂度O(n)
随着某个变量n的变化,临时空间也随之变化,空间复杂度即为O(n). demo: var nn = []; for(var i=0; i<n;i++){ nn[i] = i+1; }
周一题目
-
解题思路: 首先真的是无从下手,翻阅资料,写了关于链表的一些定义,当然都是从网上摘录的,就感觉和数组比较相像,不过,数组在内存中是一块连续的需要预留的空间。 感想:看了一圈别人的解题思路,大部分用的快慢指针的方式,还有个比较有意思,也是自己目前没理解的反转链表。
快慢方式: var hasCycle = function(head) { if(head === null){ return false; } let fast = head; let slow = head; while(fast !== null && fast.next !==null){ //慢指针每次走一步 slow = slow.next; //快指针每次走两步 fast = fast.next.next; //如果相遇,说明有环,直接返回true if (slow === fast) { return true; } } return false; }反转链表: var hasCycle = function(head) { const rev = reverseList(head); if(head !== null && head.next !== null && rev === head){ return true; } return false; } function reverseList(head) { let newHead = null; while(head !== null){ //先保存访问的节点的下一节点 //留着下一步访问 let temp = head.next; //每次访问的原链表节点都会成为新链表的头节点,其实就是把新链表挂到访问的元链表节点后面就可以了 head.next = newHead; // 更新新链表 newHead = head; // 重新赋值,继续访问 head = temp; } return newHead; } -
解题思路:由于有了上次第一题目的思路,可以拿到是否有环,那么看看在快慢指针相遇也即等于的时候能否做一些事情。 看过比较好理解的解题思路的:定义2个指针,快和慢,如果有环,快慢肯定会相遇,我们定义了快指针是慢指针速度的2倍,那么他们相遇时候慢指针走了k步数,快指针就肯定是2k步数。(肯定再环内循环走)我们定义环的入口到相遇点距离是m,那么头节点到相遇点就是k-m,而快指针
快慢方式: var detectCycle = function(head) { if(head === null){ return null; } let fast = head; let slow = head; let start = head; let meet = null; while(fast !== null && fast.next !==null){ //慢指针每次走一步 slow = slow.next; //快指针每次走两步 fast = fast.next.next; //如果相遇,说明有环, if (slow === fast) { meet = slow; break; } } if(meet === null) { return meet; } while(start !== meet){ start = start.next; meet = meet.next; } return start; } -
解题思路: 看到对应的数据结构,应该和递归有比较大的关系,先写出伪代码,再进行完善。 运行时,发现数组开辟的空间太大,直接报错,可见这代码写的是多low。
报错代码,开辟的空间太大了 var isHappy = function(n) { let nums= n.toString().split(""); let num = 0; nums.forEach(i=>{ num += Math.pow(i,2); }); if(num === 1){ return true; } if(num >(Math.pow(2,31)-1)){ return false; } return isHappy(num); }看了关于题目的讲解,有对应的快慢指针法:
var isHappy = function(n){ let slow = n; let fast = getNext(n); while(fast !== 1 && fast !== slow){ slow = getNext(slow); fast = getNext(getNext(fast)); } return fast === 1; } function getNext(n){ return n.toString().split("").map(i=>Math.pow(i,2)).reduce((a,b)=>a+b); } -
解题思路:根据第一题的思路,可以将链表进行反转,要求是迭代或者递归。
双指针 var reverseList = function(head) { let pre = null; //前置 let curr = head; // 当前节点 let temp = null; // 保存当前节点的下一节点 while(curr !== null) { temp = curr.next; curr.next = pre; // 翻转操作 //更新pre和curr的指针 pre = curr; curr = temp; } return pre; };
javascript 递归
5. leetcode: 反转链表Ⅱ
解题思路:根据现有认知,是先把对应位置的节点先删除,再插入,但是题目要求可否一次性的扫描完成。所以现有认知的情况肯定不符合,那么看下别人优秀的咋写的。
```javascript
头插法:
/**
* @param {ListNode} head
* @param {number} left
* @param {number} right
* @return {ListNode}
*/
var reverseBetween = function(head, left, right) {
if(left === right){
return head;
}
const dummy = new ListNode(-1);
dummy.next = head;
let pre = dummy;
for(let i=0; i<left-1;i++){
pre = pre.next;
}
let curr = pre.next;
for(let i =0; i<right-left; i++){
const next = curr.next;
curr.next = next.next;
next.next = pre.next;
pre.next = next;
}
return dummy.next;
};
```
周三题目
-
leetcode:K个一组翻转链表 解题思路:和周一的第五题有点相似啊,只是把right-left换成k,那么按照周一第五题的思路来试试看
var reverseKGroup = function (head, k){ if(k === 1) { return head; } const l = getLength(head); if(l=== 0 || l=== 1){ return head; } const kl = Math.floor(l/k); return insertHead(head, kl,k); } /** * 链表头插法 */ function insertHead(head, kl,k){ const dummy = new ListNode(-1); dummy.next = head; let pre = dummy; let curr = pre.next; for(let i =0; i<kl; i++){ for(let j=0;j<k-1;j++){ const next = curr.next; curr.next = next.next; next.next = pre.next; pre.next = next; } //移动pre 和curr pre = curr; curr = pre.next; } return dummy.next; } /** * 获取链表的长度 */ function getLength(head){ if(head === null) { return 0; } let l = 0; let curr = head; while(curr !== null) { l++; curr = curr.next; } return l; } } -
leetcode: 旋转链表 解题思路:如果我把它想象成一个圆环,记录一下一开始的节点,然后在圆环上顺时针转动k个位置,再把圆环按照一开始的节点拆成单链表?但是从例子上看,移动的位置如k,那么就位移k次,循环从尾部插入到头部岂不是更好?(发现循环次数多了,不通过)
也是利用圆环: var rotateRight = function (head, k) { if(head === null || k === 0){ return head; } let curr = head; let last = head; let l = 0; // 找到最后一个结点 while(curr !==null){ last = curr; curr = curr.next; l++; } if(k%l===0){ return head; } last.next = head; // 最后的节点指向开头 let move = l - (k%l); last = head; while(move>0){ curr = last; last = last.next; move--; } curr.next = null; return last; } -
leetcode: 两两交换链表中的节点 解题思路:和第一题的思路相同,把k个一组改成2个一组进行转换,小于2的话,直接返回。按照这个思路来进行解题试试~~
/** * @param {ListNode} head * @return {ListNode} */ var swapPairs = function(head) { if(head === null || head.next === null){ return head; } /** * 获取链表长度 */ let l = 0; let ll = head; while(ll!==null){ ll = ll.next; l++; } let dummy = new ListNode(-1); dummy.next = head; let pre = dummy; let curr = head; const m = Math.floor(l/2); while(m--){ const next = curr.next; cur.next = next.next; pre.next = next; next.next = cur; pre = cur; cur = pre.next; } return dummy.next; }; -
leetcode:删除链表的倒数第N个节点 解题思路:用双指针吗?是否断掉相连的节点就可以了?
/** * @param {ListNode} head * @param {number} n * @return {ListNode} */ var removeNthFromEnd = function(head, n) { if(head === null) { return head; } // 拿到head的长度 let l = 0; let ln = head; while(ln!==null){ ln = ln.next; l++; }; // 倒数第n个节点,正着数是第几个 const leftL = l-n+1; const dummy = new ListNode(-1); dummy.next = head; let pre = dummy; let curr = head; if(leftL=== 1) { pre.next = curr.next; curr.next = null; return dummy.next; } for(let i = 1; i<leftL; i++){ const next = curr.next; pre = curr; curr = next; if(leftL === i+1){ pre.next = curr.next; curr.next = null; } } return dummy.next; }; -
leetcode: 删除排序链表中的重复元素 解题思路: 看下链表的数据结构,是否是interface ListNode 中有个val 那么这个val是不是可以判断重复的值?
/** * @param {ListNode} head * @return {ListNode} */ var deleteDuplicates = function(head) { if(head === null){ return head; } let curr = head; while(curr !== null && curr.next !==null){ const next = curr.next; if( curr.val === next.val){ curr.next = next.next; next.next = null; }else { curr = next; } } return head; };