数据结构与算法

百度百科:计算机程序被定义为“一组指示计算机执行动作或做出判断的指令,通常用某种程序设计语言编写,运行于某种目标体系结构上”。

程序 = 算法 + 数据结构

关于数据结构

        数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法索引技术有关。

        数据结构(data structure)是带有结构特性的数据元素的集合,它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系,并对这种结构定义相适应的运算,设计出相应的算法,并确保经过这些运算以后所得到的新结构仍保持原来的结构类型。简而言之,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,即带“结构”的数据元素的集合。“结构”就是指数据元素之间存在的关系,分为逻辑结构和存储结构。

        关于对数据结构的学习,并不仅仅是学习其中现成的那些队列,堆栈,二叉树,图等经典结构, 也不仅仅是学习其中的那些快速排序、冒泡排序等算法。更重要的是你要学习一种思想:如何把现实问题转化为计算机语言的表示。 

        比如,学了顺序表和链表。你就知道,在查询操作更多的程序中,你应该用顺序表;而修改操作更多的程序中,你要使用链表;而单向链表不方便怎么办,每次都从头到尾好麻烦啊,怎么办?你这时就会想到双向链表or循环链表。

        学了栈之后,你就知道,很多涉及后入先出的问题,例如函数递归就是个栈模型、Android的屏幕跳转就用到栈,很多类似的东西,你就会第一时间想到:我会用这东西来去写算法实现这个功能。

        学了队列之后,你就知道,对于先入先出要排队的问题,你就要用到队列,例如多个网络下载任务,我该怎么去调度它们去获得网络资源呢?再例如操作系统的进程(or线程)调度,我该怎么去分配资源(像CPU)给多个任务呢?肯定不能全部一起拥有的,资源只有一个,那就要排队!那么怎么排队呢?用普通的队列?但是对于那些优先级高的线程怎么办?那也太共产主义了吧,这时,你就会想到了优先队列,优先队列怎么实现?用堆,然后你就有疑问了,堆是啥玩意?自己查吧,敲累了。

        总之好好学数据结构就对了。我觉得数据结构就相当于:我塞牙了,那么就要用到牙签这“数据结构”,当然你用指甲也行,只不过“性能”没那么好;我要拧螺母,肯定用扳手这个“数据结构”,当然你用钳子也行,只不过也没那么好用。学习数据结构,就是为了了解以后在IT行业里搬砖需要用到什么工具,这些工具有什么利弊,应用于什么场景。以后用的过程中,你会发现这些基础的“工具”也存在着一些缺陷,你不满足于此工具,此时,你就开始自己在这些数据结构的基础上加以改造,这就叫做自定义数据结构。而且,你以后还会造出很多其他应用于实际场景的数据结构。。你用这些数据结构去造轮子,不知不觉,你成了又一个轮子哥。

引用自:数据结构_Macro_code的博客-CSDN博客

常见数据结构

        线性表:

        线性表是指由n(n≥0)个具有相同数据类型的数据元素组成的有限序列。

L=(a_{1},a_{2},\cdots ,a_{i},a_{i+1},\cdots ,a_{n})

        L中有n个不同元素,该元素具有抽象性,仅表示逻辑关系,不表示元素具体内容。

        线性表的基本操作(java实现):

public class LineList(){

// InitList(&L):初始化表。构建一个空的线性表
// Length(L):求表长。返回线性表L的长度,即表L中的数据元素个数
// LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值e的元素
// GetElem(L,i):按位查找操作。获取在表L中第i个位置的元素的值
// ListInsert(&L,i,e):插入操作。在表L中第i个位置插入元素e
// ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除的元素
// PrintList(L):输出操作。按前后顺序输出线性表L的所有元素的值
// Empty(L):判空操作。若L位空表,则返回true,否则返回false
// DetoryList(&L):销毁操作。销毁线性表,并释放线性表L所占用内存空间

}

        栈和队列:

        数组、串和广义表:

        树和二叉树:

        二叉树:只有两个枝的树

# Definition for a binary tree node.
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

        二叉树的三种遍历方式:

        ① NLR:前序遍历(Preorder Traversal 亦称(先序遍历))

             ——访问根结点的操作发生在遍历其左右子树之前。根左右

class PreOrderTraversal:
    def preordertraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []

        def preorder(root):
            if not root:
                return
            else:
                res.append(root.val)
                preorder(root.left)
                preorder(root.right)
        preorder(root)
        return res

        ② LNR:中序遍历(Inorder Traversal)

             ——访问根结点的操作发生在遍历其左右子树之中(间)。左根右

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []
        def inorder(root:Optional[TreeNode]):
            if not root:
                return
            else:
                inorder(root.left)
                res.append(root.val)
                inorder(root.right)
        inorder(root)
        return res

        ③ LRN:后序遍历(Postorder Traversal)

             ——访问根结点的操作发生在遍历其左右子树之后。左右根

class PostOrderTraversal:
    def postordertraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []

        def postorder(root):
            if not root:
                return
            else:
                postorder(root.left)
                postorder(root.right)
                res.append(root.val)
        postorder(root)
        return res

                  堆:

        图:

        哈希表:

关于算法

        程序是指完成某些事物的一种既定方式和过程,可以将程序看成是一系列动作的执行过程的描述。而算法就是其中执行过程的原理和设计。

        换而言之,算法是针对于某种问题的解决方法,而程序是是整体和该方法的具体编码实现。

时间复杂度和空间复杂度

        算法效率分析分为两种:第一种是时间效率,第二种是空间效率。

        时间效率被称为时间复杂度,主要衡量的是算法的运行速度

        空间效率被称作空间复杂度,主要衡量一个算法所需要的额外空间,在计算机发展的早期,计算机的存储容量很小。但如今,计算机的存储容量已经不需要太过于关注了,所以算法的空间复杂度在一些空间敏感的场景在需要考虑。

      时间复杂度

        时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。 一个算法执行所耗费的时间理论上来说是算不出来的,因为它不仅仅与你写的算法有关,还与运行这个算法的机器也有关系,如果你的机器很好,那么你所耗费的时间就可能会更少,所以,一个算法耗费的时间是需要放在机器上实际测验才能知道的,但是我们总不能每个算法都拿来上机测试,来记录该算法的时间,所以我们就有了时间复杂度这样的分析方式。
        一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

        大O的线性表示法:是用于描述函数渐进行为的数学符号。

推导大O阶方法(只保留对结果影响最大的那一项来表示最终的时间复杂度):
1、用常数1取代运行时间中的所有加法常数
2、在修改后的运行次数函数中,只保留最高阶项
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶

比如,如下:

代码1:
void timeComplexity1(int N){
    int count = 0;
    for (int i = 0; i < N; i++) 
    {
        for (int j = 0; j < N; j++) { count++; }
    }
    for (int k = 0; k < 2 * N; k++) { count++; }
    int M = 10;
    while (M--) { count++; }
    System.out.println("%d\n", count);
}

        一共3个for循环和1个while循环,每循环一次我们说它进行了一次基本操作。那么这个函数执行基本操作的次数为F(N)=N²+2*N+10

        那么根据大O阶方法,该算法的时间复杂度应该是O(N²)。

代码2:
void timeComplexity2(int N, int M) 
{
    int count = 0;
    for (int k = 0; k < M; k++) { count++; }
    for (int k = 0; k < N; ++k) { count++; }
    printf("%d\n", count);
}

        这个取决于N大还是M大,所以该算法的时间复杂度是O(N+M)。

代码3:
void timeComplexity3(String str,char c)
{
    int i = 0;
    while (str.charAt(i) != '\0')
    {
        if (str.charAt(i).equals(c))
        {
            return str;
        }
        i++;
    }
}

        字符串查找函数,取决于:1、字符串多长;2、第几位才能找到。此时假定字符串长度为N。时间复杂度计算时,需要以最差情况考虑。那么该算法时间复杂度是O(N)。

代码4:
void timeComlexity4(int[] values) 
{
    int temp;
    for (int i = 0; i < values.length; i++) {
        for (int j = 0; j < values.length - 1 - i; j++) {
            //减i原因:内层循环,每循环完一趟就在数组末产生一个最大数,即最大数就不用比较了。
            if (values[j] > values[j + 1]) {
                temp = values[j];
                values[j] = values[j + 1];
                values[j + 1] = temp;
             }
        }
    }
}

        冒泡排序代码。两层循环,最差情况下,实际运算次数= N + (N - 1) + …… + 2 + 1= N*(N+1)/2。所以该算法的时间复杂度为O(N²)。

代码5:
int timeComlexity5(int[] nums, int target){
    if(nums == null || nums.length == 0){ //数组为空
        return -1;
    }
    int l = 0, r = nums.length - 1;       //设置左右边界
    while(l <= r){ 
        int mid = l + (r-l) / 2;          // mid=(l+r)/2

        if(nums[mid] == target){              //最终target=mid,输出mid
            return mid; 
        }else if(nums[mid] < target) {        //目标值在(mid,r]之间
            l = mid + 1; 
        }else {                               //目标值在[l,mid)之间
        r = mid - 1; 
        }
    }
    // 不存在
    return -1;
}

        二分查找法,一次干掉一半。最坏情况,最后一个才是我们的target。那么实际运算次数应该= 2 + 2 + …… + 2(一共log₂N个2),所以该算法的时间复杂度是O(log₂N)。

代码6:
int timeComlexity6(int n){
    return n < 2 ? n : timeComlexity6(n-1) + timeComlexity6(n-2)
}

        斐波那契数列的实现,看看要第几位。每次运算都会调用两次方法,那么实际运算次数应该是2^N-1次,所以该算法的时间复杂度为O(2^N-1)。

空间复杂度

        空间复杂度的定义:空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。

        比如,代码4-冒泡排序,一共创建了3个变量。按大O表示法,那么该算法空间复杂度为O(1)

        比如,我在方法中创建了一个数组,假设长度为N,那么因为开辟了N个空间,所以空间复杂度为O(N)

        比如,代码6-斐波那契数列,看似并没创建变量。但方法执行时,机器层面吗,方法每递归一次,就会多一层栈帧(栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。)。所以递归深度即为空间复杂度。所以该算法空间复杂度为O(N)

        实际上,空间复杂度一般只有两种情况:
                创建了常数个变量:O(1)
                创建了N个变量:O(N)

常见算法

        排序:

                时间复杂度(O(1)<O(log2n))

                按是否需要将全部记录放在内存分为内排序和外排序?

                内排序

                        插入排序、选择排序、交换排序、归并排序、基数排序

                外排序

                        希尔排序、堆排序、快速排序

        查找:

                二分查找、深度优先DFS、广度优先BFS

        递归(通过树来理解?):

                分治、回溯

        选择:

                贪心算法(最小生成树、哈夫曼编码等)、动态规划

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值