1、要掌握的:
10个数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树;
10 个算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法
算法和数据结构的关系:数据结构是为算法服务的,算法要作用在特定的数据结构之上
2、基本功:复杂度分析
我们需要一个不用具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法,了解你的代码运行质量!
所有代码的执行时间T(n) 与每行代码的执行次数成正比. 即: 每行代码的执行次数越多,所有代码的执行时间就越长. 每行代码的执行次数越少,所有代码的执行时间就越短.
从最初每行代码的时间都要计算在内,叫T(n), 大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,其忽略了低阶、常量、系数。
最终,我们分析时间复杂度 都是 说 量级 O(n)、O(n方)
如何时间复杂度分析总结:
1. 只关注循环执行次数最多的一段代码
2. 加法法则:总复杂度等于量级最大的那段代码的复杂度
3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
常见的复杂度量级如下:
数组:
是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
特点:数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
想要查找数组里的某个值,算法复杂度取决于用的什么算法,查找是查找指定的值所在的下标(存储地址),需要不断拿内存中存储的value来比较,而访问是给定下标也就是value的存储地址去获取value,直接寻址就行了。看似很相近的两个词,表述出来所表达的意思刚还是相反的。
插入:
最好情况:插入最后一位 O(1),最坏插入第一位后面的均后移 O(n),平均:(1+2+3..n)/n=O(n)
优化:某些场景下,直接调换指定的位置数据就可以了,不需要把后面的数据整体移动。如插入第3位,则 先插入最后一位,再和第三位进行调换。
删除:
最好情况:删除尾部,O(1),最坏情况,删除第一个,后面的均向前O(n),平均为 O(n)
优化:在某些特殊场景下,我们并不一定非得追求数组中数据的连续性。如果我们将多次删除操作集中在一起执行,删除的效率是会提高很多。如:先记录下已经删除的数据,每次每次的删除操作并不是真正地搬移数据,只是记录数据已经被删除。当数组没有更多空间存储数据时,我们再触发执行一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。(应用在jvm的垃圾标记删除上)
查找:
因为是寻找访问,所以支持随机查询。
c语言悬疑案例:(java有数组越界检查,因为java创建数组要先指定大小)
int main(int argc, char* argv[]){
int i = 0;
int arr[3] = {0};
for(; i<=3; i++){
arr[i] = 0;
printf("hello world\n");
}
return 0;
}
//将无限输出 hello world
数值为什么下标从0开始?
因为 a[k]_address = base_address + k * type_size,如果从1开始,每次随机访问时多一次计算,对cpu来说是多了一次指令,不过最可能的是历史原因,沿用。
链表:
插入:O(1),相当于在数值最后面插入
删除:O(1)
查找:需要一个一个往下找
占空间,删除和插入比单链表高效
总结:数组相比,链表更适合插入、删除操作频繁的场景,查询的时间复杂度较高
//链表反转
public class temp {
public static void main(String[] args) {
//创建一个点链表
SingNode head = new SingNode(0);
SingNode headTemp = head; //记录头节点, 记录后head变化后,headTemp的值并不受影响
for (int i = 1; i < 10; i++) {
SingNode nextNode = new SingNode(i);
head.next = nextNode;
head = nextNode;
}
//输出链表
soutNode(headTemp);
//链表反转的实现
SingNode headLastTemp = getLastSingNode(headTemp);
SingNode headLastTempTemp=headLastTemp;
while (headTemp != null) {
//每次获取最后一个节点
SingNode lastSingNode = getLastSingNode(headTemp);
//发现获取最后的一个节点 和头节点相等 说明 已经只有一个节点了
if(lastSingNode==headTemp){
break;
}
headLastTemp.next=lastSingNode;
headLastTemp=lastSingNode;
}
soutNode(headLastTempTemp);
}
private static SingNode getLastSingNode(SingNode headTemp) { //这个值 是传过来的是 地址, 在函数中 你可以对地址 对应的 对象 进行操作,但是对地址是修改不了的
SingNode result = null;
while (headTemp!=null) {
//说明就剩1个元素
if(headTemp.next==null){
result=headTemp;
break;
}
if (headTemp.next.next == null) {
result = headTemp.next;
//删除最后一个节点
headTemp.next = null;
break;
}
headTemp=headTemp.next;
}
System.out.println(result.data);
return result;
}
private static void soutNode(SingNode headTemp) {
while (headTemp != null) {
System.out.print(headTemp.data + " ");
headTemp = headTemp.next;
}
}
}
class SingNode {
Integer data;
SingNode next;
public SingNode(int data) {
this.data = data;
}
}
上述代码遇到的坑:想在函数中,让链表的头重置,其实是无效的,这和java的特性有关,
void A(){
B(student)
}
//是控制不了 A函数中的 student对象地址的, 但能控制 student 对象的属性
void B(Sturdent student){
student=null;
}
Java中其实还是值传递的,只不过对于对象参数,值的内容是对象的引用。
因此,基本类型是值拷贝,对象类型的是 地址值拷贝
因此,我们有了 地址值 是能找到 那个对象的,因此 我们可以对 该对象的属性 进行改动,但是休想 对 地址值改动。 向上面的代码,B想修改student的地址值,在B内是有效的值null,但是在A函数中 稳如老狗。
全局变量是存储在哪里?
存在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。(全局变量是可以看作是 某个类的 字段属性)