数据结构与算法---线性表

前言

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();
    }
}

  • 方法二:利用栈
  1. 遇到左字符,将左字符入栈
  2. 遇到右字符:

如果栈为空,说明括号无效

如果栈不为空,将栈顶字符出栈,与右字符做对比:
如果左右字符不匹配,说明括号无效
如果左右字符匹配,继续扫描下一个字符

  1. 所有字符扫描完毕后
    如果栈为空,说明括号有效
    如果栈不为空,说明括号无效
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)的时间复杂度


在这里插入图片描述

总结

在这里插入图片描述

看一下各个结构的组成:
线性表:同类型、有限序列
线性表分为:顺序存储和链式存储

顺序存储是借助数组的结构体或类
链式存储是借助指针的结构体或类

栈、队列都是线性表,因此,可以使用顺序存储实现,也可以使用链式存储实现

栈的顺序存储:
在这里插入图片描述
栈的链式存储:
在这里插入图片描述
队列的顺序存储:
在这里插入图片描述

队列的链式存储:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值