前言
MJ大神的数据结构与算法,整个3季视频有130+个小时时长,真的是膜拜!!!
程序 = 数据结构 + 算法
这也就说明了数据结构与算法的重要性
自己之前学习数据结构大致有:
上学期间学的课本严蔚敏的《数据结构》
程杰的《大话数据结构》
小甲鱼的《数据结构》
郝斌的《数据结构》
浙江大学的《数据结构》
自己学习过程中存在的问题:
知道理论知识,不能手写出代码,需加强练习,手写代码。
解决方法:熟写《剑指offer》
理论知识与实际考察点没有结合起来。也就是,自己知道栈、队列、二叉树,但是遇到实际中的问题不知道用这些知识去解决。
解决方法:熟写《剑指offer》
好多大神都说,《剑指offer》这本书的65道题要是掌握了,基本上80%的面试题都是这上面的。那么,《剑指offer》是必刷了。
开篇小例子
斐波那契数列
n = 0,1,2,3,4,5,6, 7, 8, 9…
fib=0,1,1,2,3,5,8,13,21,34…
求第n个斐波那契数列
方法一:递归
public class Main {
public static void main(String[] args)
{
System.out.print(fib(3));
}
public static int fib(int n)
{
if (n == 0){
return 0;
}else if(n == 1){
return 1;
}else {
return fib(n - 1) + fib(n-2);
}
}
}
其时间复杂度是多少呢?
答:O(2^n)
上面方法使用的递归,当n=60的时候,已经需要等待很长时间才能出结果
这是因为,每一个fib(n)都要计算,虽然前面已经计算出来了结果
因此,我们可以合理利用前面计算出来的结果,有下面的写法:
方法二:迭代
public class Main {
public static void main(String[] args)
{
System.out.print(fib(60));
}
public static int fib(int n)
{
if (n == 0){
return 0;
}else if(n == 1){
return 1;
}else {
int first = 0;
int second = 1;
for (int i = 0; i < n - 1; i++) {//为何是n-1可以试出来
int sum = first + second;
first = second;
second = sum;
}
return second;
}
}
}
该方法直接返回的是second(return second)有点不好理解,可以改进下多建立一个sum进行返回:
public static int fib(int n)
{
if (n == 0){
return 0;
}else if(n == 1){
return 1;
}else {
int first = 0;
int second = 1;
int sum = 0;
for (int i = 0; i < n - 1; i++) {//为何是n-1可以试出来
sum = first + second;
first = second;
second = sum;
}
return sum;
}
}
这样,就是return sum,好理解。
更优写法,i直接从2开始,到n结束,比较符合常规思路:
public static int fib(int n)
{
if (n <= 1){
return n;
}
int first = 0;
int second = 1;
int sum = 0;
for (int i = 2; i <= n; i++) {
sum = first + second;
first = second;
second = sum;
}
return sum;
}
时间复杂度O(n)
如何评判一个算法的好坏?
正确性、可读性、健壮性
时间复杂度
空间复杂度
最常用的且比较重要的是:时间复杂度
时间复杂度
有三个评判标准:
- 最好情况复杂度
- 最坏情况复杂度
- 平均情况复杂度
对于数组,最重要的操作就是CRUD
数组的取值、修改值的复杂度都是O(1)
数组的插入:
最好情况:插入元素在最后,不需要移动元素,最好时间复杂度O(1)
最坏情况:插入元素在最前,需要移动全部元素,最坏时间复杂度O(n)
平均情况:(1+2+…+n)/n = n(n+1)/2/n = (n+1)/2 = O(n)
同理,可以得到数组的删除操作复杂度:
最好:O(1)
最坏:O(n)
平均:O(n)
对于链表
链表的取值与修改、插入与删除都是:
最好:O(1)
最坏:O(n)
平均:O(n)
对数阶一般省略底数
因此,对于log2n或者log9n,都称为O(logn)
对于斐波那契数列,第一种方法的时间复杂度是:
开始引入主题:
什么是数据结构?
数据结构是计算机存储、组织数据的方式
数据结构大致有:线性结构、非线性结构(树、图)
线性表
线性表是n个相同类型元素的有限序列(n>=0)
最重要的两个关键点就是:同类型、有限
线性表分为:数组、链表、栈、队列、哈希表
这样分看着也没毛病
不过可以更清晰一下:
线性表按照存储方式可以分为:顺序存储和链式存储
顺序存储:利用数组的连续存储空间顺序存放线性表的各元素
利用数组,也就是顺序存储 != 数组,而是顺序存储利用了数组
在C语言中,是利用结构体
在JAVA语言中,是利用类Class
链式存储:不要求逻辑上相邻的两个元素物理上也相邻;通过“链”建立起数据元素之间的逻辑关系
数组
数组:所有元素的内存地址是连续的
对象数组里面存储的是:地址值
建立一个对象数组:Object[] objectArray = new Object[10];
该数组里面,每一个元素不是Object类型本身,而是一个指向Object类型的地址值。
针对线性表中的动态数组,有个明显的缺点:
可能会造成内存空间的浪费(每次内存不够用,就扩容1.5倍)
那么,我们可以使用非连续存储的方式:链式存储
链表
链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的。
注意边界测试
一般有0,size-1,size
数据结构与算法—线性表—反转链表
数据结构与算法—线性表—判断一个链表是否有环
数据结构与算法—线性表—链表删除某一节点
为了更好的操作链表,我们有时候会使用头结点。
头结点又称为虚拟节点
虚拟头结点里面的值为null
其实,有些图片会有误解,比如
你说,head是什么?是一个指针?一个引用?
不是的,head也是一个类,一个Class
head就是一个结点
第一个结点点,就是head
head就是一个结点,head就是第一个结点
双向链表
单向循环链表
双向循环链表
栈
栈是一种特殊的线性表,只能在一端进行操作。
往栈中添加元素的操作,一般叫做push,入栈
从栈中移除元素的操作,一般叫做pop,出栈
先进后出原则
此处的栈,指的是“先进后出”的这种数据结构
栈空间是指的内存某个模块,某个空间
Stack.Peek 与 stack.pop 的区别
相同点:大家都返回栈顶的值。
不同点:peek 不改变栈的值(不删除栈顶的值),pop会把栈顶的值删除。
有效的括号问题
判断(){}[]是否成对出现
- 方法一:
如果是有效的括号,如果相邻则变为“”,则最后会变为empty
如果是无效的括号,最后则不是空
通过循环的消除,可得如下Java代码:
class Solution {
public boolean isValid(String s) {
while(s.contains("()") || s.contains("[]") || s.contains("{}"))
{
s = s.replace("()", "");
s = s.replace("[]", "");
s = s.replace("{}", "");
}
return s.isEmpty();
}
}
- 方法二:利用栈
- 遇到左字符,将左字符入栈
- 遇到右字符:
如果栈为空,说明括号无效
如果栈不为空,将栈顶字符出栈,与右字符做对比:
如果左右字符不匹配,说明括号无效
如果左右字符匹配,继续扫描下一个字符
- 所有字符扫描完毕后
如果栈为空,说明括号有效
如果栈不为空,说明括号无效
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
int length = s.length();
for (int i = 0; i < length; i++){
char c = s.charAt(i);
if (c == '(' || c == '{' || c == '['){//如果是左字符
stack.push(c);
}else{//右字符
if (stack.isEmpty()) return false;
char tempTop = stack.pop();
if (tempTop == '(' && c != ')') return false;
if (tempTop == '[' && c != ']') return false;
if (tempTop == '{' && c != '}') return false;
}
}
if (stack.isEmpty()) {
return true;
}else{
return false;
}
}
}
队列
队列是一种特殊的线性表,只能在头尾两端进行操作
队尾(rear):只能从队尾添加元素,一般叫做enQueue,入队
队头(front):只能从队头移除元素,一般叫做deQueue,出队
遵循先进先出的原则,FIFO
如何使用栈,实现队列?
可以准备两个栈:inStack、outStack
- 入队时:push到inStack中
- 出队时:
如果outStack为空,将inStack所有元素逐一弹出,push到outStack,outStack弹出栈顶元素
如果outStack不为空,outStack弹出栈顶元素
class MyQueue {
//入栈
private Stack<Integer> inStack = new Stack<>();
//出栈
private Stack<Integer> outStack = new Stack<>();
public MyQueue() {
}
public void push(int x) {
inStack.push(x);
}
public int pop() {
if (outStack.isEmpty()) {
while(!inStack.isEmpty()){
outStack.push(inStack.pop());
}
}
return outStack.pop();
}
public int peek() {
if (outStack.isEmpty()) {
while(!inStack.isEmpty()){
outStack.push(inStack.pop());
}
}
return outStack.peek();
}
public boolean empty() {
return inStack.isEmpty() && outStack.isEmpty();
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
双端队列
双端队列是能在头尾两端添加、删除的队列
◼int size(); // 元素的数量
◼boolean isEmpty(); // 是否为空
◼void clear(); // 清空
◼void enQueueFront(E element); // 从队头入队
◼void enQueueRear(E element);// 从队尾入队
◼E deQueueFront(); // 从队头出队
◼E deQueueRear(); // 从队尾出队
◼E front(); // 获取队列的头元素
◼E rear(); // 获取队列的尾元素
循环队列
循环队列(Circle Queue)
其实队列底层也可以使用动态数组实现,并且各项接口也可以优化到O(1)的时间复杂度
总结
看一下各个结构的组成:
线性表:同类型、有限序列
线性表分为:顺序存储和链式存储
顺序存储是借助数组的结构体或类
链式存储是借助指针的结构体或类
栈、队列都是线性表,因此,可以使用顺序存储实现,也可以使用链式存储实现
栈的顺序存储:
栈的链式存储:
队列的顺序存储:
队列的链式存储: