数据结构与算法
课程介绍
数据结构和算法,一个非常古老的课题。
上学的时候就觉得数据结构和算法很难学,似乎就从来没学明白过。
工作的时候,只求程序能跑,并不太关注性能,所以尽量避坑(ArrayList Or LinkedList),哪个简单
用哪个
想跳槽去大厂或小厂,只要面试到数据结构和算法,必跪无疑
…
数据结构和算法是程序员的内功,架构搭的再好,技术使用的再新,如果没有好的数据结构设计和算
法,系统也会出问题甚至崩塌,尤其是在互联网软件上,细节决定成败!练好内功尤为重要!
本章作为基础章节,从零基础建立数据结构和算法知识体系和算法思维。包括复杂度计算,常见的数据
结构及操作,排序、递归、字符串匹配、搜索、贪心算法、分治算法、动态规划、回溯算法等。并且还
会结合大厂的数据结构和算法面试题,讲解思路和解决方法。以及剖析数据结构和算法在互联网领域的
常见应用等。
本章内容适用于初级程序员、高级程序员、架构师和一切喜欢研究算法追求细节的开发人员。
第一部分 数据结构与算法概述
第1节 数据结构的概念
1.1 什么是数据结构
数据结构(data structure)是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定
关系的数据元素的集合。(百度百科)
一句话解释:存数据的,而且是在内存中存!
、
1.2 常见的数据结构
第2节 算法的概念
2.1 什么是算法
算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用
系统的方法描述解决问题的策略机制。
一句话描述:算法是一种解决特定问题的思路
比如:LRU算法,最近最少使用,解决的就是当空间不够用时,应该淘汰谁的问题,这是一种策略,不
是唯一的答案,所以算法无对错,只有好和不好。
2.3 常见算法
第3节 算法复杂度
数据结构和算法本质上是”快“和"省"。所以代码的执行效率是非常重要的度量
我们采用时间复杂度和空间复杂度来计算
3.1 时间复杂度
大O复杂度表示法
我们假设执行一行代码的时间为t,通过估算,代码的执行时间T(n)与执行次数成正比,记做:
T(n): 代码执行时间
n:数据规模
f(n):每行代码执行次数总和
O:代码的执行时间与f(n)表达式成正比
上面的例子中的T(n)=O(2n+2)
当n无限大时,低阶、常量、系统都可以忽略
所以T(n)=O(n)
即上例中的时间复杂度为O(n),也就是代码执行时间随着数据规模的增加而增长
上例中T(n)=O(nn),也就是代码执行时间随着数据规模的增加而平方增长
即:上例中的时间复杂度为O( ) int sum(int n){ int s=0; //t int i=1; //t for(;i<=n;i++){ //tn s=s+i; //tn }return s; //t } n=100 1+1+100n+100n+1=200n+3 T(n)=O(f(n)) int sum(int n){ int s=0; int i=1; int j=1; for(;i<=n;i++){// n j=1; for(;j<=n;j++){ //nn s=s+i+j; //nn } }return s; }
时间复杂度也成为渐进时间复杂度。
计算时间复杂度的技巧
计算循环执行次数最多的代码
总复杂度=量级最大的复杂度
比如把上面两段代码合在一起
时间复杂度为O( )
嵌套代码的复杂度等于嵌套内外代码复杂度的乘积(乘法法则)
常见的时间复杂度
O(1)
这种是最简单的,也是最好理解的,就是常量级
不是只执行了一行代码,只要代码的执行不随着数据规模(n)的增加而增加,就是常量级
在实际应用中,通常使用冗余字段存储来将O(n)变成O(1),比如Redis中有很多这样的操作用来提
升访问性能
比如:SDS、字典、跳跃表等
O(logn)、O(nlogn)
2 =n
x=log n
忽略系数为logn
T(n)=O(logn)
如果将该代码执行n遍 int sum(int n){ int s=0; int i=1; int j=1; for(;i<=n;i++){ //tn s=s+i; //tn }for(;i<=n;i++){// n j=1; for(;j<=m;j++){ //nm s=s+i+j; //nm } }return s; } i = 1; while(i <= n){ i = i * 2;// 执行最多 }
则时间复杂度记录为:T(n)=O(nlogn),即O(nlogn)
快速排序、归并排序的时间复杂度都是O(nlogn)
O(n)
这个前面已经讲了,很多线性表的操作都是O(n),这也是最常见的一个时间复杂度
比如:数组的插入删除、链表的遍历等
O(m+n)
代码的时间复杂度由两个数据的规模来决定
m和n是代码的两个数据规模,而且不能确定谁更大,此时代码的复杂度为两段时间复杂度之和,
即
T(n)=O(m+n),记作:O(m+n)
O(mn)
根据乘法法则代码的复杂度为两段时间复杂度之积,即
T(n)=O(mn),记作:O(mn)
当m==n时,为O( )
3.2 空间复杂度
空间复杂度全称是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系
比如将一个数组拷贝到另一个数组中,就是相当于空间扩大了一倍:T(n)=O(2n),忽略系数。即为:
int sum(int m,int n){ int s1=0; int s2=0; int i=1; int j=1; for(;i<=m;i++){ s1=s1+i; // 执行最多 }for(;j<=n;j++){ s2=s2+j; //执行最多 }return s1+s2; }int sum(int m,int n){ int s=0; int i=1; int j=1; for(;i<=m;i++){// m j=1; for(;j<=n;j++){ //mn s=s+i+j; //mn } }return s; }
O(n),这是一个非常常见的空间复杂度,比如跳跃表、hashmap的扩容
此外还有:O(1),比如原地排序、O(n ) 此种占用空间过大
由于现在硬件相对比较便宜,所以在开发中常常会利用空间来换时间,比如缓存技术
典型的数据结构中空间换时间是:跳跃表
在实际开发中我们也更关注代码的时间复杂度,而用于执行效率的提升
第4节 为什么要学习数据结构和算法
互联网行业中数据结构和算法尤为重要
互联网软件特点:高并发、高性能、高扩展、高可用、海量数据
通关大厂面试必备技能
能够更好的使用类库
对编程的追求,精益求精
第二部分 数据结构与算法基础
第1节 线性表
线性表(Linear List)就是数据排成像一条线一样的结构,数据只有前后两个方向
1.1 数组
概念
数组(Array)是有限个相同类型的变量所组成的有序集合,数组中的每一个变量被称为元素。数组是
最为简单、最为常用的数据结构。
数组下标从零开始(Why)
存储原理
数组用一组连续的内存空间来存储一组具有相同类型的数据
(模拟内存存储)
灰色格子:被使用的内存
橙色格子:空闲的内存
红色格子:数组占用的内存
数组可以根据下标随机访问数据
比如一个整型数据 int[] 长度为5
假设首地址是:1000
int是4字节(32位),实际内存存储是位
随机元素寻址
a[i]_address=a[0]_address+i4
该公式解释了三个方面
连续性分配
相同的类型
下标从0开始
操作
读取元素
根据下标读取元素的方式叫作随机读取
更新元素
注意不要数组越界
读取和更新都可以随机访问,时间复杂度为O(1)
插入元素
有三种情况:
尾部插入
在数据的实际元素数量小于数组长度的情况下:
直接把插入的元素放在数组尾部的空闲位置即可,等同于更新元素的操作
中间插入
在数据的实际元素数量小于数组长度的情况下:
由于数组的每一个元素都有其固定下标,所以首先把插入位置及后面的元素向后移动,腾出地方,
再把要插入的元素放到对应的数组位置上。
int n=nums[2] nums[3]= 10; a[6]=10
超范围插入
假如现在有一个数组,已经装满了元素,这时还想插入一个新元素,或者插入位置是越界的
这时就要对原数组进行扩容:可以创建一个新数组,长度是旧数组的2倍,再把旧数组中的元素统
统复制
过去,这样就实现了数组的扩容。
int[] numsNew=new int[nums.length2]; System.arraycopy(nums,0,numsNew,0,nums.length); // 原数组就丢掉了,资源浪费 nums=numsNew;
删除元素
数组的删除操作和插入操作的过程相反,如果删除的元素位于数组中间,其后的元素都需要向前挪
动1位。
for(int i=p;i
public class ArrayDemo1 { int[] nums = new int[8]; public ArrayDemo1() { nums[0] = 3; nums[1] = 1; nums[2] = 2; nums[3] = 5; nums[4] = 4; nums[5] = 9; }public int get(int i) { return nums[i]; }public void update(int i, int n) {
nums[i] = n; }public void insertTail(int n) { nums[6] = n; }public void insertMiddle(int p, int n) { for (int i = nums.length-1; i >= p-1; i–) { //能取得值 if (nums[i] != 0) { nums[i+1]=nums[i]; } }nums[p-1]=n; }/
时间复杂度
读取和更新都是随机访问,所以是O(1)
插入数组扩容的时间复杂度是O(n),插入并移动元素的时间复杂度也是O(n),综合起来插入操作的时间
复杂度是O(n)。
删除操作,只涉及元素的移动,时间复杂度也是O(n)
优缺点
优点:
数组拥有非常高效的随机访问能力,只要给出下标,就可以用常量时间找到对应元素
缺点:
插入和删除元素方面。由于数组元素连续紧密地存储在内存中,插入、删除元素都会导致大量元素被迫
移动,影响效率。 (ArrayList LinkedList )
申请的空间必须是连续的,也就是说即使有空间也可能因为没有足够的连续空间而创建失败
如果超出范围,需要重新申请内存进行存储,原空间就浪费了
应用
数组是基础的数据结构,应用太广泛了,ArrayList、Redis、消息队列等等。
数据结构和算法的可视化网站:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
1.2 链表
概念
链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。
链表中数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元
素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据
域,另一个是存储下一个结点地址的指针域。(百度百科)
常见的链表包括:单链表、双向链表、循环链表
单链表
单向链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节
点的指针next
demo1.display(); } }
双向链表
双向链表的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev指针。
循环链表
链表的尾节点指向头节点形成一个环,称为循环链表
存储原理
数组在内存中的存储方式是顺序存储(连续存储),链表在内存中的存储方式则是随机存储(链式存
储)。
链表的每一个节点分布在内存的不同位置,依靠next指针关联起来。这样可以灵活有效地利用零散的碎
片空间。
Node{int data; Node next; }Node{int data; Node next; Node prev; }
链表的第1个节点被称为头节点(3),没有任何节点的next指针指向它,或者说它的前置节点为空
头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表
链表的最后1个节点被称为尾节点(2),它指向的next为空
操作
查找节点
在查找元素时,链表只能从头节点开始向后一个一个节点逐一查找。
更新节点
找到要更新的节点,然后把旧数据替换成新数据
插入节点
尾部插入
把最后一个节点的next指针指向新插入的节点即可
头部插入
第1步,把新节点的next指针指向原先的头节点
第2步,把新节点变为链表的头节点
中间插入
第1步,新节点的next指针,指向插入位置的节点
第2步,插入位置前置节点的next指针,指向新节点
只要内存空间允许,能够插入链表的元素是无限的,不需要像数组那样考虑扩容的问题
删除节点
尾部删除
把倒数第2个节点的next指针指向空即可
头部删除
把链表的头节点设为原先头节点的next指针即可
中间删除
把要删除节点的前置节点的next指针,指向要删除元素的下一个节点即可
完整代码
package com.lagou.line.linkedlist; /*** 单链表 / public class SingleLinkedList { //初始化头节点 private Node head = new Node(0, “”); /** 添加节点:从头插入 ** @param node / public void addNode(Node node) { //从头插入
Node tmp = head; while (true) { //到尾节点 if (tmp.next == null) { break; }//后移一个节点 tmp = tmp.next; }tmp.next = node; }public void addByIdOrder(Node node){ //从头插入 Node tmp = head; while (true) { //到尾节点 if (tmp.next == null) { break; }//节点存在 if (tmp.next.id == node.id) { break; }if (tmp.next.id > node.id) { break; }tmp = tmp.next; }//交换位置 node.next = tmp.next; tmp.next = node; }//遍历链表 public void showList() { //空链表 if (head.next == null) { System.out.println(“链表为空”); return; }Node temp = head.next; while (true) { if (temp == null) { return; }System.out.println(temp); //指针下移 temp = temp.next; } }public static void main(String[] args) {
Node n1=new Node(1,“张飞”); Node n2=new Node(2,“关羽”); Node n3=new Node(3,“赵云”); Node n4=new Node(4,“黄忠”); Node n5=new Node(5,“马超”); SingleLinkedList sll=new SingleLinkedList(); sll.addByIdOrder(n4); sll.addByIdOrder(n5); sll.addByIdOrder(n1); sll.addByIdOrder(n2); sll.addByIdOrder(n3); sll.showList(); } }package com.lagou.line.linkedlist; /** 节点 / public class Node { int id; String name; //下一个节点 Node next; public Node(int id, String name) { this.id = id; this.name = name; }@Override public String toString() { return “Node{” + “id=” + id + “, name=’” + name + ‘’’ + ‘}’; } }package com.lagou.line.linkedlist; /** 节点 / public class Node2 {
时间复杂度
查找节点 : O(n)
插入节点:O(1)
更新节点:O(1)
删除节点:O(1)
优缺点
优势
插入、删除、更新效率高
省空间
劣势
查询效率较低,不能随机访问
应用
链表的应用也非常广泛,比如树、图、Redis的列表、LRU算法实现、消息队列等
数组与链表的对比
数据结构没有绝对的好与坏,数组和链表各有千秋。
int id; String name; //下一个节点 Node2 next; //上一个节点 Node2 prev; public Node2(int id, String name) { this.id = id; this.name = name; }@Override public String toString() { return “Node{” + “id=” + id + “, name=’” + name + ‘’’ + ‘}’; } }
数组的优势在于能够快速定位元素,对于读操作多、写操作少的场景来说,用数组更合适一些
链表的优势在于能够灵活地进行插入和删除操作,如果需要在尾部频繁插入、删除元素,用链表更合适
一些
数组和链表是线性数据存储的物理存储结构:即顺序存储和链式存储。
1.3 栈
栈和队列都属于线性数据的逻辑存储结构
概念
栈(stack)是一种线性数据结构,栈中的元素只能先入后出(First In Last Out,简称FILO)。
最早进入的元素存放的位置叫作栈底(bottom),最后进入的元素存放的位置叫作栈顶 (top)。
存储原理
栈既可以用数组来实现,也可以用链表来实现
栈的数组实现如下:
数组实现的栈也叫顺序栈或静态栈
栈的链表实现如下:
链表实现的栈也叫做链式栈或动态栈
操作
入栈(压栈)
入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素的位置将会成为
新的栈顶
出栈(弹栈)
出栈操作(pop)就是把元素从栈中弹出,只有栈顶元素才允许出栈,出栈元素的前一个元素将会
成为新的栈顶。
完整代码
数组实现
package com.lagou.line.stack; /** 数组实现 / public class ArrayStack { private int[] nums; // 数组 private int count; // 栈中元素个数 // 初始化数组,申请一个大小为n的数组空间 public ArrayStack(int n) { this.nums = new int[n]; this.count = 0; }// 入栈操作 public boolean push(int n) { // 数组空间不够了,直接返回false,入栈失败。 没有扩容 // nums.len2 arraycopy if (count >= nums.length) return false; // 将item放到下标为count的位置,并且count加一 nums[count] = n; count++; return true; }// 出栈操作 public int pop() { // 栈为空,则直接返回0 if (count == 0) return 0; // 返回下标为count-1的数组元素,并且栈中元素个数count减一 int n = nums[count-1]; count–;
return n; } public static void main(String[] args) { ArrayStack as=new ArrayStack(8); as.push(3); as.push(5); as.push(1); as.push(4); System.out.println(as.pop()); System.out.println(as.pop()); System.out.println(as.pop()); System.out.println(as.pop()); } }
链表实现
package com.lagou.line.stack; /*** 链表节点 / public class Node { int value; Node next; public Node(int value) { this.value = value; } }/** 链表实现 / public class LinedStack { int size; Node head; //头节点 /** 初始化 / public LinedStack() { head = null; size = 0; }/*
时间复杂度