时间复杂度的含义 :假设每行代码执行时间是一定的,都是unit,那有一个很重要的规律:
所有代码的执行时间 T(n) 与所有行代码的执行次数 n 成正比:T(n) = O(f(n))
注意:这里仅仅从代码行数的角度考虑,所以你是简单的计算,还是从磁盘读取100M的文件,我们粗略认为两者执行时间都是unit;
时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。类比一下,空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。
T(n) = O(1)
O(logn): 实例: i=1; while (i <= n) { i = i * 2; }
O(n) :一次遍历
O(nlogn):一次遍历里面嵌套 O(logn)de实例
O(n2)场景:两次遍历
有项目之前会进行性能测试,再做代码的时间复杂度、空间复杂度分析,是不是多此一举呢?
1)空间h和时间复杂度分析为我们提供了一个很好的理论分析的方向,并且它是宿主平台无关的,能够让我们对我们的程序或算法有一个大致的认识。
2)渐进式时间,空间复杂度分析只是一个理论模型,只能提供给粗略的估计分析,我们不能直接断定就觉得O(logN)的算法一定优于O(n)
总结:性能测试和时间、空间复杂度应该相辅相成。
length为n的数组,有一个值为a,需要找到a对应下标,从下标0开始遍历查找
最大时间复杂度 :场景最坏的时间复杂度,即数组最后是a,查了n次
最小时间复杂度 :场景最优的时间复杂度,数组第一个就是啊,查了一次
平均时间复杂度:综合考虑所有场景及每个场景出现的概率得出的结果,每个位置概率为1/n;查询次数分别为 1,2,3...;平均查找次数1/n*(1+2+3+..)=(n+1)/2,故为O(n)
平摊时间复杂度:大小为n的ArrayList,插入数据,超出容量需扩容;1-n次插入都是直接插入,但是第n+1次插入需要,先扩容,后迁移n次,但是如果将最后一次平摊到所有n次插入,复杂度就是O(1),所以整体O(1)
为什么数组的下标从0开始?
a[k]_address = base_address + (k)*type_size
java中[int]是正常存成的
又:
C 语言设计者用 0 开始计数数组下标,之后的 Java、JavaScript 等高级语言都效仿了C语言;
a[k]_address = base_address + (k)*type_size
那java中String[2]上面的公式怎么确定的呢?
String的话,数组中存的是地址,而不是值
这是因为:编程语言中的”数组“并不完全等同于,我们在讲数据结构和算法的时候,提到的”数组“。编程语言在实现自己的”数组“类型的时候,并不是完全遵循数据结构”数组“的定义,而是针对编程语言自身的特点,做了调整。
可参考:https://mp.weixin.qq.com/s/E-c41h2v_AfffrlAQpkyLg
单向链表:
节点:存储数据 + 记录下一节点的指针
头结点:记录链表的基地址
尾节点:链表最后一个节点,节点指向空地址NULL
循环链表:
循环链表的尾节点指向链表的头结点
双向链表:
节点: 记录上一节点的指针+数据+记录下一节点的指针
双向链表好处:
1)可以方便获取节点的前节点,以节点删除为例:
删除给定指针指向的节点 p
单向链表:需要先从第一个遍历,找到p的前节点node.next=p;
双向链表:直接定位p的前节点:node =p.before ;
有序链表查找,可以根据大小来判断查找的方向;
如何判断两个单向链表是否相交?直接遍历两个链表,尾部相同就相交;
cpu缓存机制中的数组和链表:
数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。
用空间换时间的设计思想
数组 链表
插入删除 n 1
随机访问 1 n
链表与 FIFO LFU LRU
LFU least frequently used:最少使用策略
LRU least recently used:最近使用策略
LRU怎么实现?
链表 :新加入放入头部
数据已经存在,原节点移除,新节点放到头部
数据未存在:
如果已经达到最大值,先删除尾部,后插入头结点;
未达到最大值,直接
如何判断一个字符串是否是回文字符串的问题,我想你应该听过,我们今天的题目就是基于这个问题的改造版本。如果字符串是通过单链表来存储的,那该如何来判断是一个回文串呢?你有什么好的解决思路呢?相应的时间空间复杂度又是多少呢?
单链表存储呢? --快慢指针遍历+前半部分链表反转
数组存储呢? 直接 i和n-1-i比较
栈存储呢? 遍历压栈 然后出战与原链表比较
链表思路好掌握,但代码十分难写,我精选了5个常见的链表操作。你只要把这几个操作都能写熟练,不熟就多写几遍,我保证你之后再也不会害怕写链表代码。
单链表反转
求链表的中间结点
两个有序的链表合并
删除链表倒数第n个结点
链表中环的检测
链表的代码写法,注意考虑边界条件
空 1节点 2节点 操作头结点 操作尾节点
链表反转:
直接反转源链表:
1 暂存node的next
2 改变node的next指向
3 暂存反转好的部分,为下次反转做准备
4 node走到next
链表合并部分:
一定是先找到起始节点,然后开始合并,否则理清很难
环监测的思考
1 被指向次数即可,存放到head中 时间复杂度O(n),空间复杂度O(n)
2 O(1)解决此问题,
1)node中value存放被指向次数即可
2)双指针法
代码实例:
public class LinkedTest {
public static void main(String[] args) {
// // 正序删除节点
// Node node6 = new Node('f',null);
// Node node5 = new Node('e',node6);
// Node node4 = new Node('d',node5);
// Node node3 = new Node('c',node4);
// Node node2 = new Node('b',node3);
// Node node1 = new Node('a',node2);
// ergodicNode(node1);
// Node ret = deleteNode(node1, 1);
// ergodicNode(ret);
// ret = deleteNode(ret, 5);
// ergodicNode(ret);
// ret = deleteNode(ret, 3);
// ergodicNode(ret);
// 倒序删除节点
// Node node6 = new Node('f',null);
// Node node5 = new Node('e',node6);
// Node node4 = new Node('d',node5);
// Node node3 = new Node('c',node4);
// Node node2 = new Node('b',node3);
// Node node1 = new Node('a',node2);
// ergodicNode(node1);
// Node ret = deleteBackNode(node1, 6);
// ergodicNode(ret);
// ret = deleteBackNode(ret, 1);
// ergodicNode(ret);
// ret = deleteBackNode(ret, 3);
// ergodicNode(ret);
// 判断回文:偶数个节点测试
// Node node6 = new Node('a',null);
// Node node5 = new Node('b',node6);
// Node node4 = new Node('c',node5);
// Node node3 = new Node('c',node4);
// Node node2 = new Node('b',node3);
// Node node1 = new Node('a',node2);
// System.out.println(isPlindrome(node1));
// System.out.println(isPlindromeNew(node1));
// ergodicNode(node3);
// ergodicNode(node4);
// 判断回文:奇数个节点测试
// Node node5 = new Node('a',null);
// Node node4 = new Node('b',node5);
// Node node3 = new Node('c',node4);
// Node node2 = new Node('b',node3);
// Node node1 = new Node('a',node2);
// System.out.println(isPlindrome(node1));
// System.out.println(isPlindromeNew(node1));
// ergodicNode(node3);
// ergodicNode(node4);
// 合并测试
// Node node6 = new Node('e',null);
// Node node5 = new Node('c',node6);
// Node node4 = new Node('b',node5);
// Node node3 = new Node('d',null);
// Node node2 = new Node('c',node3);
// Node node1 = new Node('a',node2);
// ergodicNode(node1);
// ergodicNode(node4);
// Node node = mergeLinked(node1, node4);
// ergodicNode(node);
// ergodicNode(node1);
// ergodicNode(node4);
// 节点插入与删除
// Node node5 = new Node('e',null);
// Node node4 = new Node('d',node5);
// Node node3 = new Node('c',node4);
// Node node2 = new Node('b',node3);
// Node node1 = new Node('a',node2);
// insertSecond(node1);
// ergodicNode(node1);
// deleteSecond(node1);
// ergodicNode(node1);
// 指针反转调用实例
// Node node5 = new Node('e',null);
// Node node4 = new Node('d',node5);
// Node node3 = new Node('c',node4);
// Node node2 = new Node('b',node3);
// Node node1 = new Node('a',node2);
// ergodicNode(node1);
// Node newLinked = reverseOriginLinked(node1);
// // 输出 e,d,c,b,a, 说明返回的指针确实是反转后的
// ergodicNode(newLinked);
// // 输出a 说明确实是原链表被反转了
// ergodicNode(node1);
// 判断有环
Node node4 = new Node('d',null);
Node node3 = new Node('c',node4);
Node node2 = new Node('b',node3);
Node node1 = new Node('a',node2);
node3.setNext(node1);
System.out.println(getCycleWithHash(node1));
System.out.println(getCycleWithDouPoint(node1));
}
/**
* desc:统计指向次数,头结点被指向或非头结点被指向两次
*/
public static int getCycleWithHash(Node node){
HashMap nodes = new HashMap<>();
Node head = node;
int i =0;
while(node != null){
i++ ;
node = node.next;
if(node == head){
return 0;
}
if(nodes.containsKey(node)){
return nodes.get(node);
}else{
nodes.put(node, i);
}
}
return -1;
}
/**
* desc:快慢指针法,环内总能相遇
*/
public static boolean getCycleWithDouPoint(Node node){
Node head = node;
Node fast = node;
Node slow = node;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
return true;
}
}
return false;
}
/**
* desc:删除链表第n个结点
*/
public static Node deleteBackNode(Node node, int n){
int length = getNodeLength(node);
// 倒序转正序,如删除倒数第一个,就是删除正序第n个
int sequeal = length + 1 - n;
return deleteNode(node,sequeal);
}
/**
* desc:删除链表第n个结点
*/
public static Node deleteNode(Node node, int n){
int length = getNodeLength(node);
if(n <=0 || n>length){
return node;
}
Node ret = node;
// 删除头
if(n == 1){
node = node.next;
return node;
}
// 删除尾
if(n == length){
while(n > 2){
node = node.next;
n--;
}
node.next = null;
return ret;
}
// 中间删除
// 这里很特别,采坑
while(n>2){
node = node.next;
n--;
}
node.next = node.next.next;
return ret;
}
/**
* desc:链表合并
*/
public static Node mergeLinked(Node node1, Node node2){
if(node1 == null ){
return node2;
}
if(node2 == null ){
return node1;
}
// 先找到起始点,这个很关键,否则合并部分很难理清
Node tmp = null;
Node ret = null;
if(node1.getValue() > node2.getValue()){
ret = node2;
tmp = node2;
node2 = node2.next;
}else{
ret = node1;
tmp = node1;
node1 = node1.next;
}
while(node1 != null && node2 != null){
if(node1.getValue() < node2.getValue()){
// 改变指向
tmp.next= node1;
// 向后移动
tmp = tmp.next;
node1 = node1.next;
}else{
tmp.next= node2;
tmp = tmp.next;
node2 = node2.next;
}
}
//尾部处理,初次书写就丢了
if(node1 == null){
tmp.next= node2;
}else{
tmp.next= node1;
}
return ret;
}
/**
* desc:插入节点,未考虑空,两次修改指针
*/
public static void insertSecond1(Node node ){
Node s = new Node('s', null);
Node originSecond = node.next;
node.next = s;
s.next = originSecond;
}
/**
* desc:考虑空,一次修改指针
*/
public static void insertSecond(Node node ){
if(node == null){
node = new Node('s', null);
}else{
Node s = new Node('s', node.next);
node.next = s;
}
}
public static void deleteSecond(Node node ){
if(node == null || node.next == null){
node = null;
}else{
node.next = node.next.next;
}
}
/**
* desc:链表反转,直接反转源链表
*/
public static Node reverseOriginLinked(Node node){
Node newLinked = null;
Node node1 = null;
while(node != null){
// 暂存指针
node1 = node.next;
// 反转指针
node.next = newLinked;
// 保存反转后的结果
newLinked = node;
// 指针前进
node = node1;
}
return newLinked;
}
/**
* desc:创建新的链表,达到反转的效果,本质上原链表未修改
*/
public static Node reverseLinked(Node node){
Node start = new Node(node.getValue(), null);
while(node.next != null){
node = node.next;
start = new Node(node.getValue() ,start);
}
return start;
}
/**
* desc:遍历链表,打印值
*/
public static void ergodicNode(Node node){
while(node != null){
System.out.print(node.getValue()+",");
node = node.next;
}
System.out.println();
}
/**
* desc:判断是否 回文字符串,通过建立新的节点实现,不推荐
*/
public static boolean isPlindrome(Node node){
Node fast = new Node(node.getValue(),node.getNext());
Node slow = new Node(node.getValue(),node.getNext());
Node slowTemp = new Node(slow.getValue(),null);
while( fast != null && fast.getNext() != null ){
fast = fast.getNext().getNext();
slow = slow.getNext();
slowTemp = new Node(slow.getValue(),slowTemp);
}
// 偶数个函数调整
int i = 0;
while( node != null){
node = node.getNext();
i++;
}
if(i%2 == 0){
slowTemp = slowTemp.getNext();
}
while(slow != null && slowTemp !=null){
if(slow.getValue() != slowTemp.getValue()){
return false;
}
slow = slow.getNext();
slowTemp = slowTemp.getNext();
}
return true;
}
/**
* desc:假设偶数个节点
*/
public static boolean isPlindromeNew(Node node){
if(node == null){
return false;
}
boolean isOdd = isOddSize(node);
Node fast = node;
Node slow = node;
Node reverse = null;
Node tmp = null;
while(fast != null && fast.next != null){
fast = fast.next.next;
tmp = slow.next;
slow.next = reverse;
reverse = slow;
slow = tmp;
}
// 奇数长度过中间节点不比较,直接跳过
if(isOdd){
slow = slow.next;
}
while(reverse != null){
if(reverse.getValue() != slow.getValue()){
return false;
}
reverse = reverse.next;
slow = slow.next;
}
return true;
}
/**
* desc:获取节点长度
*/
public static int getNodeLength(Node node){
int length = 0;
while(node != null){
node = node.next;
length ++;
}
return length;
}
/**
* desc:判断是否奇数个长度
*/
public static boolean isOddSize(Node node){
int length = getNodeLength(node);
if(length % 2 == 1){
return true;
}
return false;
}
/**
* desc:定义指针节点
*/
static class Node{
char value;
Node next;
public Node(char value, Node next){
this.value = value;
this.next = next;
}
public char getValue(){
return value;
}
public Node getNext(){
return next;
}
public void setNext(Node next){
this.next = next;
}
}
}