zoukankan      html  css  js  c++  java
  • 数据结构与算法02--如何设计合理的数据结构

    参考资料:拉勾网课程《重学数据结构与算法》 公瑾

    本笔记中代码全部使用C#实现,C#也很易读,不妨碍其他语言的用户参考。但本笔记的参考资料,也就是《重学数据结构与算法》这门课程中的代码是Java实现。

    如何灵活使用数据结构?数据处理的基本操作有哪些?

    课程中有一个例题非常不错。有一个数组[1,2,3,4,5,5,6],查出其中出现次数最多的元素的值。显然该数组出现次数最多的元素的值为5,它出现了两次。
    普通的做法:

    static void Main(string[] args)
    {
        int[] arr = {1,2,3,4,5,5,6};
        int val_max = -1;   // 出现次数最多的元素的值
        int time_max = 0;   // 出现次数最多的元素的出现次数
        int time_tmp = 0;   // 临时储存当前元素的出现次数
    
        for (int i=0; i<arr.Length; i++)
        {
            time_tmp = 0;
            for (int j=0; j<arr.Length; j++)
            {
                if (arr[i] == arr[j])
                {
                    time_tmp += 1;
                }
                if (time_tmp > time_max)
                {
                    time_max = time_tmp;
                    val_max = arr[i];
                }
            }
        }
    
        Console.WriteLine(val_max);
    }
    

    输出的结果为5,结果正确。考虑一下如何优化这段代码。上一篇数据结构的文章中提到过利用复杂度来优化程序的三步:

    1. 暴力解法:在没有限制的情况下完成开发。
    2. 处理无效操作:删掉代码中的无效存储和无效计算,降低时间和空间复杂度。
    3. 时空转换:设计合理数据结构,将时间复杂度转移到空间复杂度上。

    这段代码相当于已经完成了步骤1。分析一下这段代码,这段代码中没有无效计算,需要考虑数据结构的一些方法来进行优化,把时间复杂度转移到空间复杂度上。代码有两层for循环,时间复杂度为O(n^2),我们考虑用一个for循环来解决问题。这时候往往就能想到大学的数据结构课程中讲过的“字典”这个数据结构。

    为何要使用字典?

    先分析一下此处代码需要对数据进行哪些操作:

    1. 根据原始数组计算每个元素出现的次数;
    2. 根据1.的结果,找到出现次数最多的元素。

    步骤1.

    如何处理1.统计出现的次数呢?该选择哪种数据结构来存储呢?假设我们此时还没有选择字典作为数据结构,我们选择数据结构A。

    对每一次的for循环,都得到数组中的元素arr[i],每次循环得到它之后,就要判断它在数据结构A中是否出现过。

    1. 如果出现了,就需要对出现的次数加1。
    2. 如果没有出现过,则把这个元素新增到数据结构A中,并且把次数赋值为1。

    一共进行过三种数据操作:

    1. 查找:看能否在数据结构A中查找到这个元素,即判断元素是否出现过。
    2. 新增:若没有出现过,新增这个元素。
    3. 改动:若出现过,对这个元素出现的次数加1。

    步骤2.

    上面已经分析完步骤1.的操作,再看步骤2.。步骤2.是访问数据结构A中的各个元素,找到出现次数最多的元素。只涉及查找操作。

    假设数据结构A为数组,使用两个数组分别按照对应顺序记录arr中的元素和对应元素的出现次数。想查找数组中的元素只能逐一访问,时间复杂度是O(n)。在O(n)复杂度的for循环中,又嵌套了O(n)复杂度的查找动作,所以时间复杂度是 O(n^2),没有变化。

    字典类型的数据结构能在O(1)时间复杂度内完成查找操作。字典的查找是通过键值对的匹配完成的。先通过一次for循环,将数组转变为元素(Key)-出现次数(Value)的一个字典。再去遍历一遍这个字典,找到出现次数最多的那个元素,就能找到最后的结果了。

    static void Main(string[] args)
    {
        int[] arr = { 1, 2, 3, 4, 5, 5, 6 };
        var dic = new Dictionary<int, int>();
    
        for (int i = 0; i < arr.Length; i++)
        {
            if (dic.ContainsKey(arr[i]))
            {
                dic[arr[i]]++;
            }
            else
            {
                dic.Add(arr[i], 1);
            }
        }
    
        int val_max = -1;   // 出现次数最多的元素的值
        int time_max = 0;   // 出现次数最多的元素的出现次数
    
        foreach (var kv in dic) // kv是键值对的意思,也就是字典中的一项
        {
            if (kv.Value > time_max)
            {
                time_max = kv.Value;
                val_max = kv.Key;
            }
        }
    
        Console.WriteLine(val_max);
    }
    

    课程中的代码是Java实现。我对Java了解不多,只会一些基础语法,不敢多数,只是觉得课程中代码使用字典的时候确实有点憨,甚至引入了一个其他变量才能最终实现这个功能。使用C#的字典时没有那么麻烦。并不是说C#比Java好,Java用户很多,所以这门语言不可能不好,如果不好,哪来的这么多用户,但因为我是一个编程初学者,没有资格评判这些,所以不再多说。

    上述代码实现了跟起初的那段代码同样的功能,但这段代码的两个for/foreach不是嵌套关系,也就是说时间复杂度是O(n)+O(n)=O(2n),也就还是O(n)。通过采用更复杂且高效的数据结构,完成了时间复杂度到空间复杂度的转移,提高了空间复杂度,成功把时间复杂度指数级的下降了。

    再看空间复杂度。无论使用数组还是字典,都需要额外的存储空间来存储数据,空间复杂度都为O(n)。

    如何设计合理的数据结构?

    从问题本身出发,我们可以采用这样的思考顺序:

    • 分析这段代码到底对数据先后进行了哪些操作。
    • 根据分析出来的数据操作,找到合理的数据结构。

    理清了数据处理的基本操作,更复杂的问题也是这些基本操作的叠加和组合。只要按这种逻辑进行思考,就可以轻松设计出合理的数据结构。

    代码对数据的处理就是代码对输入的数据进行计算,得到结果并输出。数据处理的操作就是找到需要处理的数据,计算结果,保存结果。套路如下:

    • 找到待处理的数据。即按照条件进行查找
    • 将结果存到新的内存空间中。即在现有数据上进行新增
    • 将结果存到一个已使用的内存空间中。先删除内存空间中的已有数据,再新增新的数据。

    很复杂的代码对数据的处理也只有这3个基本操作,增、删、查。只要围绕这3个操作进行分析,就能得出解决问题的最优方案。分析方法:

    • 这段代码对数据进行了哪些操作?
    • 这些操作中,哪个操作最影响效率,对时间复杂度的损耗最大?
    • 哪种数据结构最能帮助你提高数据操作的使用效率?

    查找、新增、删除

    查找就是从数据结构中找到满足某个条件的元素的过程。一般有两种:

    1. 根据元素的位置或索引来查找
    2. 根据元素的数值特征来查找

    数组的查找:
    从数组arr中查找数组的第n个元素,也就是n-1号arr[n-1],时间复杂度为O(1),因为数组有索引,有index,可以直接找到。

    链表的查找:
    链表和数组的空间复杂度都是O(n),但数组有index的索引,链表没有。链表通过指针,按某个顺序连在一起。要查找其第n个元素,就必须要先知道第一个元素在哪里,按照第一个元素的next指针寻找第二个元素,然后继续找下去。只能通过从前往后的顺序一个一个去查找。因为链表因为没有索引。查找的时间复杂度是O(n)。

    如果要查找数据结构中数值等于v的元素是否存在。这样,数组和链表就都不行了,需要按顺序一个一个判断当前元素数值是否等于v,时间复杂度为O(n)。但如果借助“字典”这种数据结构,就能在O(1)内完成查找。我个人感觉就像给数据结构加上了索引。这也顺便让我理解了为什么给数据库加索引能提高访问速度,为什么Redis这么快。

    除了查找,还有新增和删除。

    在复杂数据结构中,在尾端新增一条数据,对原先的数据结构没有影响。首先查找到最后一个数据的位置,然后在这个位置之后,通过新增操作赋值或插入一条新数据。

    在原数据结构中间新增一条数据,会对原数据结构产生影响,即插入元素的位置之后的元素数据的位置依次加1。

    删除也一样,分为尾端删除和中间删除。

    如何分析操作过程?

    在某个复杂数据结构中,在第2个元素之后新增一条数据。随后再删除第1个满足数值大于6的元素。分析操作过程,有两个步骤的操作:

    1. 在第2个元素之后新增一条数据。包含了查找和新增两个操作,查找第二个元素的位置,并在数据结构中间新增一条数据。
    2. 删除第1个数值大于6的元素。包含查找和删除两个操作,即查找出第二个数值大于6的元素的位置,并删除这个位置的元素。

    因此,需要完成的操作包括按照位置的查找、新增和按照数据数值的查找、删除。这就是一个典型的分析操作过程的步骤。

    学拉勾网《重学数据结构与算法》这门课程到这里,我学到了如何使用“数据结构”。感觉学校的课程只是教了我们一些数据结构,但我完全不知道怎么用,什么时候用。公瑾老师的这门课算是我学习数据结构与算法的引导课,引领我入门。目前课程就更新到这里,也是希望能尽快更新,我现在是求知若渴,哈哈。公瑾老师不愧是搞机器学习算法的,搞通用算法也是非常厉害。看来用“内功”来形容数据结构与算法,是一点也不虚。搞会了它们,就锻炼了人的一种解决问题的思路,无论是在编程中做什么,都是用得到的。

    笔记中或多或少出现了一些课程的付费内容,感觉我的笔记有帮助的朋友,不妨也去购买一波课程,两位数的价格非常便宜。为知识付费,支持更多高质量的内容产出。

  • 相关阅读:
    算法与数据结构(十五) 归并排序(Swift 3.0版)
    算法与数据结构(十四) 堆排序 (Swift 3.0版)
    算法与数据结构(十三) 冒泡排序、插入排序、希尔排序、选择排序(Swift3.0版)
    算法与数据结构(十二) 散列(哈希)表的创建与查找(Swift版)
    算法与数据结构(十一) 平衡二叉树(AVL树)(Swift版)
    算法与数据结构(十) 二叉排序树的查找、插入与删除(Swift版)
    算法与数据结构(九) 查找表的顺序查找、折半查找、插值查找以及Fibonacci查找(Swift版)
    算法与数据结构(八) AOV网的关键路径(Swift版)
    算法与数据结构(七) AOV网的拓扑排序(Swift版)
    算法与数据结构(六) 迪杰斯特拉算法的最短路径(Swift版)
  • 原文地址:https://www.cnblogs.com/Kit-L/p/12969757.html
Copyright © 2011-2022 走看看