数据结构与算法的个人学习经验小结(引子)

  我们先用leetcode上的一个例子作为一个引子,感受一下数据结构还有算法之间的微妙关系。
  闲话不多说直接上题:Given an array of integers, every element appears twice except for one. Find that single one. (译:给定一组整数,每个元素都出现了两次,只有一个出现了一次,找到出现一次的那个数。)原题对于你的程序是有限制条件的,在这里为了行文方便,暂时先略过。
  在没有限制条件的情况下,应该很直接的可以想到对数组中的每个元素统计其出现的次数,如果你此时对一些数据结构还不够了解,那么你可能会采用这种方法:从第一个元素开始,每个元素都与除它之外的其他数组元素进行比较,用一个变量记录元素出现的次数,比较完成后检测其出现的次数是否为 1 ,返回出现次数为 1 的元素。简单代码如下:

    int singleNumber(int A[], int n) {
    if (n == 0) return 0;
    for (int i = 0; i < n; i++)
    {
        int temp = 1;//初始化次数为1,如果与自身也进行比较的话,初始化次数为0
        for (int j = 0; j < n; j++)
        {
            if (i == j) continue;
            if (A[j] == A[i]) temp++;
        }
        if (temp == 1) return A[i];
    }
    return 0;
}

  这种方法比较直接,容易理解,但是稍显笨重,因为有两层循环。那么有没有更为简单些的方法呢,当然有,如果你已经有一定的数据结构基础,你可能会想到哈希表,即建立一个元素与其出现次数对应的哈希表,然后从哈希表中返回出现一次的元素。代码如下:

int singleNumber(int A[], int n) {
        if(n==0) return 0;
    int *hash = new int[n];
    for (int i = 0; i < n; i++) hash[i] = 0;
    for (int i = 0; i<n; i++) hash[A[i]]++;
    for (int i = 0; i < n; i++) if (hash[A[i]] == 1) return A[i];
    return 0;
}

  这种方法相较于第一种方法,最直观的变化是,其循环不是嵌套的,也就是只有一层循环,虽然它有三个循环,但是其时间复杂度是更低的。不过这种方法也有它不好的地方,首先需要额外的动态数组去记录每个元素出现的次数,而且最为致命的是,如果数组A中的最大值比元素数目n大的话,那么大于n的数字是无法被记录的。所以,上面的代码程序其实是错误的,但是它提供了一个思路,就是建立一个新的数组去记录每个元素出现的次数,如果延续上面的方法,对其进行修改,那么可以先找到A中的最大值,进而建立足够大的数组,但是这样的话,既浪费时间(寻找最大值……),也浪费空间(可能需要非常大数组)。
  为了引出数据结构的作用,我又用了另外一个方法,这种方法的思想和上面的方法是一致的,但是却用到了新的工具。代码如下:

int singleNumber(int A[], int n) {
    if (n == 0) return 0;
    map<int, int> m;
    for (int i = 0; i<n; i++) m[A[i]]++;
    for (auto j : m) if (j.second == 1) return j.first;
    return 0;
}

  上面的代码中出现了一个新的工具结构map,map在具备哈希性质的同时,更加的灵活,其不用一次性为map 分配空间,并且不用初始化。但是同样的,这种方法也必须申请大量的额外空间,不过虽然如此,这种方法依托map数据结构带来的便捷性已经能充分体现出来了。这也是我前篇博文提到的工具使用的重要性的直观体现。
  然而有时候,你掌握了一些工具,也不见得就能解决问题,如果缺乏必要的算法思想的话。现在我们加上原题的限制条件:Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory? (译:你的算法必须满足线性时间复杂度,并且不能申请额外的空间。)有了这个限制条件,我们发现上面所述的方法都不满足要求,方法一不是线性时间复杂度,方法二和三虽然满足线性时间复杂度,但是显然不满足不能申请额外空间的条件。因为算法本身不是本篇介绍的重点,因此这里就没有其他引导性质的论述,直接给出满足要求的代码,代码如下:

int singleNumber(int A[], int n) {
       if(n==0) return 0;
       int &re = A[0];
       while(n>1)
       {
          re = re^A[n-1]; //A[0]=A[0]^A[n-1] 最后一句return A[0]
          n--;
      }
      return re;
}

  这段代码的核心是异或位运算的使用。由于两个相等的数异或运算结果为零,与零异或运算后的结果与原数相等。在这两条性质的帮助下,执行完整的异或运算循环之后,得到的结果自然就是与出现一次的元素相等的数。这里就充分体现出算法在解决问题中的作用性了,同时也体现出,想要实现一个好的算法,你不仅要有好的逻辑思维,还需要具备过硬的基础知识。比如本例中,如果你不懂得异或运算,也就无法写出这样一个算法了。
  本篇文章通过一个例子,直观展现了算法实现的几个要点。相信读者看到这也大概能找到自己比较欠缺的点在什么地方了。
  最后要说的是,这几个要点对于你我这样的普通人来说都是需要积累的,需要锤炼的。即使是天赋较高的人,也只是在时间上会占些优势。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值