数据结构与算法常用总结

1.常用数据结构及其原理

在这里插入图片描述

1.1 数组

在这里插入图片描述
每一个数组元素的位置由数字编号,称为下标或者索引(index)。大根据维度区分,有 2 种不同的数组:一维数组和多维数组(数组的元素为数组)
特点:

  • 数组的元素具有统一的类型;必须全为int或全为float
  • 数组的下标具有固定的上下界,一旦被定义,维数和维界不可更改;
  • 数组储存的数据占内存中连续的一段,用标识符标识;一个numpy的array是内存中的一个连续块,array中的元素都是同一类型的,所以一旦确定了一个array,它的内存就确定了,每个元素的内存大小都确定了。

1.2 链表

是一种线性表,但是并不会按线性的顺序存储数据,而是在由一个个节点组成,每个节点(node)中储存着数据变量(data)和指针变量(node next),又有一个头节点(head)连接下面的节点,而最后一个节点指向空(null)。可以在链表类中定义增加,删除,插入,遍历,修改等方法。
优点
(1).使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。
(2).数据的存取往往要在不同的排列顺序中转换,而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。
缺点:链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

1.3 队列

队列是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
在队列这种数据结构中,最先插入的元素将是最先被删除的元素;反之最后插入的元素将最后被删除的元素,因此队列又称为“先进先出”(FIFO—first in first out)的线性表。

队列的方法
add(E e) 将指定的元素插入此队列(如果立即可行且不会违反容量限制),在成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。返回boolean。

element() 获取不移除此队列的头,如果此队列为空,则抛出NoSuchElementException,返回泛型E。

offer(E e) 将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时, 此方法通常要优于 add(E),后者可能无法插入元素,而只是抛出一个异常。返回boolean。

peek() 获取不移除此队列的头,如果此队列为空,则返回 null。返回泛型E。

poll() 获取并移除此队列的头,如果此队列为空,则返回 null。返回泛型E。

remove() 获取并移除此队列的头。返回泛型E。

class Queue(object):
    """队列"""
    
    def __init__(self):
        self.__item = []

    def enqueue(self,item):
        """往队列中添加一个item元素"""
        self.__item.insert(0,item)

    def dequeue(self):
        """从队列头部删除一个元素"""
        if self.__item:
            return self.__item.pop()
        else:
            return None

    def is_empty(self):
        """判断一个队列是否为空"""
        return self.__item == []

    def size(self):
        """返回队列的大小"""
        return len(self.__item)


if __name__ == "__main__":
    q = Queue()
    q.enqueue("hello")
    q.enqueue("world")
    q.enqueue("itcast")
    print(q.size())
    print(q.dequeue())
    print(q.dequeue())
    print(q.dequeue())
    
    
# 运行结果
3
hello
world
itcast
class Deque(object):
    """双端队列"""

    def __init__(self):
        self.__item = []

    def add_front(self,item):
        """从队头加入一个item元素"""
        self.__item.insert(0,item)

    def add_rear(self,item):
        """从队尾加入一个item元素"""
        self.__item.append(item)

    def remove_front(self):
        """从队头删除一个item元素"""
        if self.__item:
            return self.__item.pop(0)
        else:
            return None

    def remove_rear(self):
        """从队尾删除一个item元素"""
        if self.__item:
            return self.__item.pop()
        else:
            return None

    def is_empty(self):
        """判断双端队列是否为空"""
        return self.__item == []

    def size(self):
        """返回队列的大小"""
        return len(self.__item)




if __name__ == "__main__":
    deque = Deque()
    deque.add_front(1)
    deque.add_front(2)
    deque.add_rear(3)
    deque.add_rear(4)
    print(deque.size())
    print(deque.remove_front())
    print(deque.remove_front())
    print(deque.remove_rear())
    print(deque.remove_rear())
  
  
# 运行结果
4
2
1
4
3

1.4 栈

在这里插入图片描述
1.栈(Stack),是硬件。它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
栈是允许在同一端进行插入和删除操作的特殊线性表。允许进行插入和删除操作的一端称为栈顶(top),另一端为栈底(bottom);栈底固定,而栈顶浮动;栈中元素个数为零时称为空栈。插入一般称为进栈(PUSH),删除则称为退栈(POP)。栈也称为先进后出表
2、栈(Stack)是操作系统在建立某个进程或者线程时(在支持多线程的操作系统中是线程)为这个线程建立的存储区域。
2. 栈的方法

empty() 测试堆栈是否为空。返回boolean。

peek() 查看堆栈顶部的对象,但不从堆栈中移除它。返回泛型E。

pop() 移除堆栈顶部的对象,并作为此函数的值返回该对象。返回泛型E。

push(E item) 把项压入堆栈顶部。返回泛型E。

search(Object o) 返回对象在堆栈中的位置,以 1 为基数。返回int。

class Stack(object):
    """栈"""

    def __init__(self):
        self.__item = []

    def push(self,item):
        """添加一个新的元素item到栈顶"""
        self.__item.append(item)

    def pop(self):
        """弹出栈顶元素"""
        return self.__item.pop()


    def peek(self):
        """返回栈顶元素"""
        if self.__item:
            return self.__item[-1]
        else:
            return None

    def is_empty(self):
        """判断栈是否为空"""
        return self.__item == []

    def size(self):
        """返回栈的元素个数"""
        return len(self.__item)


if __name__ == "__main__":
    stack = Stack()
    stack.push("hello")
    stack.push("world")
    stack.push("itcast")
    print(stack.size())
    print(stack.peek())
    print(stack.pop())
    print(stack.pop())
    print(stack.pop())

# 运行结果
3
itcast
itcast
world
hello

1.5 堆(Heap):

1、堆是在程序运行时,而不是在程序编译时,请求操作系统分配给自己某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。

2、堆是指程序运行时申请的动态内存,而栈只是指一种使用堆的方法(即先进后出)。栈是先进后出的,但是于堆而言却没有这个特性,两者都是存放临时数据的地方。 对于堆,我们可以随心所欲的进行增加变量和删除变量,不要遵循什么次序,只要你喜欢。

1.6 哈希表

哈希表是一种根据关键码去寻找值的数据映射结构,该结构通过把关键码(Key value)映射的位置去寻找存放值的地方。无论哈希表总中有多少条数据,插入和查找的时间复杂度都是为O(1),因为哈希表的查找速度非常快。

解决哈希冲突的方法

1) 线性探测法

2) 平方探测法

3) 伪随机序列法

4) 拉链法

1.7 二叉树

1.7.1 B树(非二叉树)

1.关键字集合分布在整颗树中;
2.任何一个关键字出现且只出现在一个结点中;
3.搜索有可能在非叶子结点结束;
4.其搜索性能等价于在关键字全集内做一次二分查找,查找复杂度为h*log(n);
5.自动层次控制;

优点:层级结构较低,且冗余节点较少

1.7.2.B+树
1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
2.不可能在非叶子结点命中;
3.B+跟B树不同B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加;
4.更适合文件索引系统;
支持翻页:每个磁盘块存储一个节点,称为一页。连续查询多个节点则称为翻页

B树和B+树的区别
(1)B+树中只有叶子节点会带有指向记录的指针,而B树则所有节点都带有,在内部节点出现的索引项不会再出现在叶子节点中。
(2)B+树中所有叶子节点都是通过指针连接在一起,而B树不会。

- B+Tree对比BTree的优点

(1)磁盘读写代价更低。B+树非叶子节点不会带上指向记录的指针,这样,一个块中可以容纳更多的索引项,一是可以降低树的高度。二是一个内部节点可以定位更多的叶子节点。搜索速度快
(2)查询速度更稳定,由于B+Tree非叶子节点不存储数据(data),因此所有的数据都要查询至叶子节点,而叶子节点的高度都是相同的,因此所有数据的查询速度都是一样的,均衡的。
(3)所有数据均有序存储在叶子节点,叶子节点之间通过指针来连接,使得范围查找、排序查找、去重查找变得简单易行(B树数据分布在各个节点,包括非叶子节点,不便于范围等查找,需要中序遍历)
(4)B+树只要遍历叶子节点就可以实现整棵树的遍历,而其他的树形结构 要中序遍历才可以访问所有的数据。
(4)缺陷:因为有冗余节点数据,因此会造成内存的浪费。

1.7.3 红黑树的规则:
1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的。
4)如果一个结点是红的,那么它的俩个儿子都是黑的。
5)对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点。

为什么平衡树和红黑树的区别是什么?为什么有了平衡树还要设计出来红黑树?
平衡树(AVL)更平衡,结构上更加直观,时间效能针对读取而言更高,但是维护起来比较麻烦!!!(插入和删除之后,都需要rebalance)。但是,红黑树通过它规则的设定,确保了插入和删除的最坏的时间复杂度是O(log N) 。

设计红黑树的目的,就是解决平衡树的维护起来比较麻烦的问题, 红黑树,读取略逊于AVL,维护强于AVL,每次插入和删除的平均旋转次数应该是远小于平衡树。

红黑树 和 b+树的用途有什么区别?
(1)红黑树多用在内部排序,即全放在内存中的,STL的map和set的内部实现就是红黑树。
(2)B+树多用于外存上时,B+也被成为一个磁盘友好的数据结构。

1.7.4 平衡二叉树
平衡二叉树是采用二分法思维把数据按规则组装成一个树形结构的数据,用这个树形结构的数据减少无关数据的检索,大大的提升了数据检索的速度;平衡二叉树的数据结构组装过程有以下规则:
(1)非叶子节点只能允许最多两个子节点存在。
(2)每一个非叶子节点数据分布规则为左边的子节点小当前节点的值,右边的子节点大于当前节点的值(这里值是基于自己的算法规则而定的,比如hash值);
总结平衡二叉树特点:

(1)非叶子节点最多拥有两个子节点;

(2)非叶子节值大于左边子节点、小于右边子节点;

(3)树的左右两边的层级数相差不会大于1;

(4)没有值相等重复的节点;

2.常见数据结构面试题

2.1数组和链表的区别

1、数组和链表的区别。

从逻辑结构上来看,数组的大小一旦定义就不能改变。当数据增加时,可能超过原先定义的元素的个数;当数据减少时,造成内存浪费;链表动态进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。

从内存存储的角度看;数组从栈中分配空间(用new则在堆上创建),对程序员方便快速,但是自由度小;链表从堆中分配空间,自由度大但是申请管理比较麻烦。

从访问方式类看,数组在内存中是连续的存储,因此可以利用下标索引进行访问;链表是链式存储结构,在访问元素时候只能够通过线性方式由前到后顺序的访问,所以访问效率比数组要低。

2.2 排序算法

在这里插入图片描述
快排算法的改进:
只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。

选择基准元的方式

最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列。

  • 方法1 固定基准元

    如果输入序列是随机的,处理时间是可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。

  • 方法2 随机基准元

    这是一种相对安全的策略。由于基准元的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n^2)
    。实际上,随机化快速排序得到理论最坏情况的可能性仅为
    1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。

  • 方法3 三数取中

    引入的原因:虽然随机选取基准时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取基准。

    分析:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为基准元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准元。

冒泡排序算法的改进

  • 1.设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。
  • 2.传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者), 从而使排序趟数几乎减少了一半。

2.3 栈结构与队列的区别

栈(stack):限定只能在表的一端进行插入和删除操作的线性表。
队列(queue):限定只能在表的一端插入和在另一端进行删除操作的线性表。
1)队列先进先出,栈先进后出。
2)对插入和删除操作的“限定”不同。
3)遍历数据速度不同。队列遍历数据的速度要快得多。

2.4 堆、栈、队列之间的区别是?

1、堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。

2、栈就是一个桶,后放进去的先拿出来,它下面本来有的东西要等它出来之后才能出来。(后进先出)

3、队列只能在队头做删除操作,在队尾做插入操作.而栈只能在栈顶做插入和删除操作。(先进先出)

2.5 Python中的堆栈

1、内存中的堆栈和数据结构中的堆栈不是一个概念,可以说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。
2、内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
​3、代码区:存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。
4、静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
5、栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
6、堆区:new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。

2.6 设计模式(属于编码规范?

什么是设计模式?
设计模式是一种代码设计经验,目的是为了可重用代码,提高代码的可重用性和可维护性。设计模式的提出基于以下两个原则:
开闭原则: 软件应用对扩展开放,对修改关闭。在增加功能时,尽量不要修改代码,只增加代码。
里式替换原则: 如果调用父类的方法可以成功,那么调用子类的方法也应该完全可以运行。

设计模式的分类:

  • 创建型模式:创建对象时,不再由我们直接实例化对象;而是根据特定场景,由程序来确定创建对象的方式。简单工厂模式(并不是23种设计模式之一)、工厂方法、抽象工厂模式、单例模式、生成器模式和原型模式。
  • 结构型模式:用于帮助将多个对象组织成更大的结构。结构型模式主要有适配器模式adapter、桥接模式bridge、组合器模式component、装饰器模式decorator、门面模式、亨元模式flyweight和代理模式proxy。
  • 行为型模式:用于帮助系统间各对象的通信,以及如何控制复杂系统中流程。行为型模式主要有命令模式command、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式state、策略模式、模板模式和访问者模式。

2.6.1 单例模式-创建型
保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式的目的是保证在一个进程中,某个类有且仅有一个实例。
单例的构造方法必须是private,防止调用方自己创建实例,在类的内部,使用静态字段来引用唯一创建的实例的。提供一个静态方法返回这个唯一的实例,或者直接把这个静态变量暴露给外部。

单例模式主要有如下两个优势:

  1. 减少创建Java实例所带来的系统开销

  2. 便于系统跟踪单个Java实例的生命周期、实例状态等。

2.6.2 工厂方法 Factory Method-创建型
定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。工厂方法模式使用特殊的工厂方法代替对于对象构造函数的直接调用。工厂方法返回的对象通常称为“产品”。
工厂方法的目的是使得创建对象和使用对象分离,并且客户端总是引用抽象工厂和抽象产品。

总结:工厂方法是指定义工厂接口和产品接口,但如何创建实际工厂和实际产品被推迟到子类实现,从而使调用方只和抽象工厂和抽象产品打交道。实际使用更多的是更简单的静态工厂方法。调用方尽量持有接口或抽象类,避免持有具体的子类,以便工厂方法能随时切换不同的子类返回,却不影响调用方代码。

2.6.3 代理Proxy-结构型
为其他对象提供一种代理以控制对这个对象的访问。当客户端代码需要调用某个对象时,客户端实际上不关心是否准确得到该对象,它只要一个能提供该功能的对象即可,此时我们就可返回该对象的代理(Proxy)。
代理模式和适配器(Adapter)模式很类似。适配器模式用于把A接口转换为B接口。

总结:代理模式通过封装一个已有接口,并向调用者返回相同的接口类型,能让调用者在不改变任何代码的前提下增强某些功能。使用Proxy要求调用方持有接口,作为Proxy的类也必须实现相同的接口类型。

2.6.4 观察者模式-行为型
定义对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
观察者模式又称为“订阅-发布模式”(Publish-Subscribe),它是一种发送通知的机制,让发送通知的一方(被观察者)和接收通知的一方(观察者)能彼此分离、互不影响。
举一个电商网站的例子:
有多种商品(Product),同时,消费者(Consumer)和管理员(Admin)对商品上架和价格变动都感兴趣,希望第一时间得到通知。

广义的观察者模式包括所有消息系统。消息系统把观察者和被观察者完全分离,通过消息系统本身来通知。消息发送方称为Producer,接收方称为Consumer,Producer发送消息时,必须选择发送到哪个Topic。Consumer可以订阅自己感兴趣的Topic,从而只获得特定类型的消息。

2.7 字节序 大小尾端

字节序,又称端序,尾序,英文:Endianness。在计算机科学领域中,字节序是指存放多字节数据的字节(byte)的顺序,典型的情况是整数在内存中的存放方式和网络传输的传输顺序。Endianness有时候也可以用指位序(bit)。

一般而言,字节序指示了一个UCS-2字符的哪个字节存储在低地址。如果LSByte在MSByte的前面,即LSB为低地址,则该字节序是小端序;反之则是大端序。在网络编程中,字节序是一个必须被考虑的因素,因为不同的处理器体系可能采用不同的字节序。在多平台的代码编程中,字节序可能会导致难以察觉的bug。

对于多字节数据,如整数(32位机中一般占4字节),在不同的处理器的存放方式主要有两种,以内存中0×0A0B0C0D的存放方式为例,分别有以下几种方式:

大端序:大端(高位)优先存储。从最高有效位字节到最低有效位字节的顺序。
地址增长方向 →
… 0×0A 0×0B 0×0C 0×0D …
最高有效位(MSB, Most Significant Byte)是0×0A 存储在最低的内存地址处。下一个字节0×0B存在后面的地址处。正类似于十六进制字节从左到右的阅读顺序。

小端序:小端(低位)优先存储。从最低有效位字节到最高有效位字节的顺序存储对象。
地址增长方向 →
… 0×0D 0×0C 0×0B 0×0A …
最低有效位(LSB,Least Significant Byte)是0×0D 存储在最低的内存地址处。后面字节依次存在后面的地址处。

在这里插入图片描述

2.8 用两个栈实现一个队列

Python剑指offer之两个栈实现一个队列-两个队列实现一个栈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值