算法学习随笔 1_数据结构和算法

算法学习随笔系列文章旨在记录和梳理本人学习算法过程当中遇到的问题和知识点,同时希望可以和大家交流,共同学习。作为一名学习者,难免有错误的地方,希望大家指正。

本系列一边整理常见数据结构,一边整理该数据结构的相关算法题,持续更新......

目录

1.什么是数据结构和算法

2.算法复杂度分析

2.1什么是算法的复杂度

2.2算法复杂度的表示

 2.3算法复杂度分析举例-递归算法

3.数组

4.链表

5.哈希表

6.栈和队列

7.二叉树


1.什么是数据结构和算法

数据结构是相互之间存在一种或者多种特定关系的数据元素的集合,算法是解决待定问题求解步骤的描述。当一个问题摆在我们面前时,我们一般首先想到要设计一个算法去解决这个问题,设计算法的过程中,涉及到对数据的处理,而合理高效的组织数据对于实现一个优秀的算法有着至关重要的作用,数据结构正是帮助我们合理高效组织数据的利器。

数据结构可以从逻辑结构和存储结构两个方面划分。逻辑结构包括集合结构,线性结构,树形结构,图形结构。存储结构包括顺序存储结构和链式存储结构。

将数据结构按照我们常用的数据结构划分有数组、栈、队列、链表、树、散列表、堆、图八大类。

针对这八类数据结构在3-10节分别进行简单梳理归纳。

2.算法复杂度分析

对于算法的复杂度分析的认识其实处于一种比较模糊的状态,对于如何计算时间、空间复杂度还不是很熟练,我身边也有一些小伙伴说对于复杂度分析有点模棱两可,所以这里对复杂度分析进行一个梳理总结。

2.1什么是算法的复杂度

算法的复杂度分为时间复杂度空间复杂度。时间复杂度是一个描述算法运行时间的一个函数,定性不定量;空间复杂度用来定性度量算法在运行过程中占用内存空间的大小,是一个大体的预估。

2.2算法复杂度的表示

时间复杂度和空间复杂度都用$O(n)、 $O(nlog n)等来表示,其中O表示的是上界,也就是算法的最坏运行情况。但是一般认为 O 表示的是一个算法的一般最坏运行情况,并不是真正的最坏情况。就拿快速排序来说,时间复杂度最坏情况是O(n^2),但是我们一般说快速排序的时间复杂度是O(n log n)。而且针对不同的数据形式和数据量,时间复杂度会受到影响,并不见得O(n)一定优于O(n^2),具体看图就可以明白。但是在数据规模足够大且通常情况下,认为复杂度可以按如下排序:O(1)常数阶 < $O(\log n)$对数阶 < $O(n)$线性阶 < $O(n^2)$平方阶 < $O(n^3)$立方阶 < $O(2^n)$指数阶

 2.3算法复杂度分析举例-递归算法

递归算法的时间复杂度=递归的次数 * 每次递归中的操作次数

递归算法的空间复杂度=每次递归的空间复杂度 * 递归深度

递归算法的时间复杂度不一定是对数阶,同一道使用递归算法的题目,有的同学可以写出$O(n)$的代码,有的同学可以写出$O(\log n)$的代码,关键在于对于时间复杂度计算方法的理解上。

例如求x的n次方,不同的写法有着不同的时间复杂度。

// 时间复杂度为O(n)
int function2(int x, int n) {
    if (n == 0) {
        return 1; // return 1 同样是因为0次方是等于1的
    }
    return function2(x, n - 1) * x;
}
//计算:每次n-1,递归了n次时间复杂度是$O(n)$,每次进行了一个乘法操作,乘法操作的时间复杂度一个常数项$O(1)$,所以这份代码的时间复杂度是 $n × 1 = O(n)$。
// 时间复杂度为O(log n)
int function4(int x, int n) {
    if (n == 0) {
        return 1;
    }
    int t = function4(x, n / 2);// 这里相对于function3,是把这个递归操作抽取出来
    if (n % 2 == 1) {
        return t * t * x;
    }
    return t * t;
}
// 计算:仅有一个递归调用,且每次都是n/2 ,所以这里我们一共调用了log以2为底n的对数次。每次递归了做都是一次乘法操作,这也是一个常数项的操作,即时间复杂度为O(log n)

3.数组

数组属于线性表,特点是结构中的元素本身可以是具有某种结构的数据,一维数组在存储空间上是连续的,通过下标来进行查找。二维及以上数组在存储空间上是否连续取决于不同的编程语言,例如在C++中二维数组是连续的,而在Java中不是,每一行的头结点地址没有规律性,不是连续的。

特点:数组下标都是从0开始的;数组内存空间的地址是连续的。

优点:数组可以方便的通过下标索引的方式获取到下标下对应的数据,查询效率较高。

缺点:数组的元素是不能删的,只能覆盖,且需要移动其他元素,所以增加和删除数据效率较低;数组元素只能为同一类型,且长度固定以后不可扩容。

4.链表

链表属于线性表,形式上有单链表,双链表,循环链表等。链表是通过指针来串联的线性结构。

单链表:每一个节点都由两部分组成,数据域和指针域。其中数据域用来存放该节点的数据,指针域用来指向下一个节点(注意是整个节点)。最后一个节点的指针指向 NULL。

 双链表:在单链表中一个节点只能指向下一个节点,只能按照一个方向遍历。在双链表中有一个数据域和两个指针域,其中一个指针指向下一个节点,另一个指针指向前一个节点,这样遍历的方向就既可以向前又可以向后。

 循环链表:首尾相接的单链表或者双链表就是循环链表。最后一个节点又指向的头结点。

链表的存储特点:链表在内存中存储时,节点不像数组那样在物理上是存放在一块连续的内存空间中,链表的节点在内存中是散乱的存放的,具体如何分配取决于不同的操作系统。链表中的节点的访问由指针域来引导。

如何定义一个链表:在C或者C++中需要手动定义,其实一个链表的节点是通过一个结构体来定义的。下面是C++定义的链表,定义完成之后,需要初始化节点。可以使用默认的构造函数也可以使用自定义的构造函数。

// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

//初始化:自定义构造函数
ListNode* head = new ListNode(5);

//初始化:默认构造函数
ListNode* head = new ListNode();
head->val = 5;

优点:插入、删除数据效率高O(1)级别,因为只需更改指针指向即可

缺点:随机访问效率低O(n)级别,因为需要从链头至链尾进行遍历;内存空间消耗更大,因为每个存储数据的节点都需要额外的空间存储后继指针。

5.哈希表

在元素的存储位置和其关键字之间建立某种直接关系,那么在进行查找时,就无需做比较或做很少次的比较,按照这种关系直接由关键字找到相应的记录。数组就是一种散列表。哈希表一般用来解决需要快速的判断某一个元素是否出现在集合中的问题。有三种常见的哈希结构,分别是数组,set集合,map映射。数组我们比较熟悉了,主要看一下set和map。

set:有三种,set、multiset、unordered_set

 map:有三种,map、multimap、unordered_map

优点:读写效率显著提升,尤其是unordered_set 和 unordered_map。

缺点:哈希法主要是用空间来换取了时间,而且存在哈希碰撞等问题,处理不好会导致资源消耗很大。

6.栈和队列

栈:栈是一种受约束的线性表,只允许栈顶元素入栈和出栈。也就是说最先入栈的元素最后才能出栈,最后入栈的元素第一个出栈,也就是后入先出、先入后出。栈顶是最后一个元素,栈底是第一个入栈的元素,也就是表尾端称为栈顶,表头端称为栈底,不含元素的空表称为空栈。栈的一个典型应用就是递归程序的调用。

        有关于栈的操作(C++):stack

        入栈:push(x)
        出栈:pop()        (出栈操作只是删除栈顶元素,并不返回该元素)
        访问栈顶:top()        (只是访问元素)
        判断栈空:empty()        (栈空返回true)
        访问栈中的元素个数:size()

队列:受约束的线性表,只允许在队尾插入,在队头删除。第一个进入队列的元素总是第一个弹出队列,先进先出,后进后出。队列头是第一个进入队列的元素的位置,队列尾是最后一个进入队列的元素的位置 。队列的典型应用是多线程阻塞队列管理。

        有关于队列的操作(C++):queue

        入队:push(x)        (将x 接到队列的末端)
        出队:pop()        (弹出队列的第一个元素,注意,并不会返回被弹出元素的值)
        访问队首元素:front()        (即最早被压入队列的元素)
        访问队尾元素:back()        (即最后被压入队列的元素)
        判断队列空:empty()        (当队列空时,返回true)
        访问队列中的元素个数:size()

双端队列:deque,可以在两端进行push和pop操作。

//pop和push
deque.push_back(element); //容器尾部添加一个数据
deque.push_front(element); //容器头部插入一个数据
deque.pop_back();         //删除容器最后一个数据
deque.pop_front();     //删除容器第一个数据

//迭代器操作
deque.begin();  //返回容器中第一个元素的迭代器。
deque.end();   //返回容器中最后一个元素之后的迭代器。
deque.rbegin();  //返回容器中倒数第一个元素的迭代器。
deque.rend();   //返回容器中倒数最后一个元素之后的迭代器。
deque.cbegin();  //返回容器中第一个元素的常量迭代器。
deque.cend();   //返回容器中最后一个元素之后的常量迭代器。

优先级队列: 优先级队列也是队列的一种,分为最大优先级队列和最小优先级队列。最大优先级队列:在队列最前面的永远是优先级最高的。最小优先级对列:在队列最前面的永远是优先级最低的。

优先级队列的实现方式:

Sorted array:在获取最大值和最小值的时候,效率非常高,复杂度为O(1),然而,由于在插入的时候需要按照顺序,导致插入操作非常慢,复杂度为O(n)

2 平衡二叉搜索树:在获取最大值和最小值的时候复杂为O(logn),插入元素时,效率比 sortedArray高一些,为 O(logn)

3 堆(heap):堆是构建优先级队列比较好的方式,在获取最大值和最小值的时候,复杂度为O(1),其他的操作复杂度都为O(logn)

7.二叉树

是一种非线性的数据结构,由n个有限结点组成有层次关系的集合。一棵树的每个结点具有0个或多个子结点、每个子结点只有一个父结点、没有前驱的结为根结点、除了根结点外,每个子结点又可以由m棵不相关的子树组成。每个节点最多只有一个父节点,不含有子节点的节点称为叶子节点

二叉树是一种特殊的树,二叉树的父节点最多只有两个子节点,左节点和右节点,并且是有序的。二叉树下还有很多分类,都具有不同的特性:满二叉树,完全二叉树,平衡二叉搜索树等等。

        满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。

        完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1)  个节点。

        平衡二叉搜索树:又被称为AVL树,它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树。

二叉树的存储方式:链式存储 - 使用指针的方式将一棵树串起来。顺序存储 - 使用数组存储。

二叉树的遍历方式:

按照遍历的方法有两大类:递归法和迭代法;

按照遍历的顺序有两大类:深度优先(一般用栈)和广度优先(一般用队列)。其中深度优先可以按照中间节点的顺序分为前序遍历(中左右)、中序遍历(左中右)、后续遍历(左右中),深度优先可以使用递归法和迭代法。广度优先就是层次遍历,一层一层的遍历,一般使用迭代法。

 二叉树的定义:这里容易忽视,因为一般刷题的时候都是定义好的,手动写可能会有点生疏

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值