【数据结构与算法】-> 社群分享

Ⅰ 前言

这篇文章的缘起,是我参加的极客大学算法训练营,要做一次社群分享,这篇文章作为底稿,大致梳理一下我想要分享的两个部分:位运算和链表。由于现在时间有限,只先梳理一个大概的总结,在后面的文章中我会将这三个部分以及其他模块比如BFS,DFS,DP,二分,贪心等等补充详细。基础知识补充在我的数据结构与算法已经发出的相关文章中,相关力扣题的整理再单独发出。

Ⅱ 链表

链表是数据结构里最最基础的一个结构,之所以想跟大家分享这么基础的东西,是因为链表的很多题是挺有趣的,我刚开始做的时候还经常绕进去,所以想分享出来几道比较有特点的题以及借由这些题的一些我的思考。

基础的快慢指针判断环,递归合并有序链表这些的我就不再赘述,相信大家已经练得很熟悉了,我就从我印象最深的一道题开始吧,就是链表的插入排序。(#147)(https://leetcode-cn.com/problems/insertion-sort-list/)对链表进行插入排序

题目要求非常直白,就是对链表做一个插入排序。我们知道,插入排序的操作就是维护一个有序集合和一个无序集合,每次从后面的无序集合中选择一个数据,按序插到有序集合中,维持前面集合的有序性。

在这里插入图片描述
思路很简单,但是用链表做,我第一次看的时候反正比较懵,感觉怎么操作都不舒服。那我们就一起来看一下这道题。

关于链表的操作,首先要思考的一个就是,我们需不需要一个dummy结点,抑或是叫它头结点,这里我分享一下我对头结点的思考。

我们可以把头结点看作是一个哨兵,它的作用在我看来有两点,一个是记录位置,一个是让每个结点的插入删除操作保持一致,不用特殊处理。这两个其实也可以当一回事来看,需要记录位置的情况发生在我们要返回头结点的条件下,如果我们对链表的结点做了移动,头结点的位置可能会发生改变,这时候我们在更改之前就需要用一个dummy结点提前先记录好头结点的位置,一般的操作就是:
在这里插入图片描述
加上这么一个结点之后,原先的头结点的(第一个有效结点)的插入删除操作就不用做特殊判断了,可以和其他结点保持一致,最后要返回这个链表的头结点直接返回 dummy.next就好。

在对链表做插入排序时,我们可能要改变头结点的位置,因为它不一定是最小的结点,同时,在排序完成之后,我们也要返回该链表的头结点,所以我们要先创建一个dummy结点。排序的主要操作就是下面这样:

在这里插入图片描述
代码的逻辑就是找到一个比上一个结点小的结点,也就是nxt,然后再从第一个结点开始遍历,找到第一个比nxt结点大的结点,也就是p.next。这时候我们就可以把nxt结点插到 pp.next的中间。

第一次做这里的时候,之前我以为自己对指针挺熟悉的了,但是对下面四行代码当时还是觉得指针和魔法一样,为什么nxt结点完成了插入以后可以和没事人一样再被赋值成cur.next继续操作? nxt.next = p.next这个操作不是已经把nxt这个结点固定到一个位置上了吗,为什么又可以通过 nxt = cur.next nxt挪开,这样不会影响到之前的结点吗? 。

后来有个同学问我关于链表的题,我给她讲的时候突然清晰了一件事,就是这个 '.‘ 到底做了什么。我们知道链表的实现,按Java来举例,就是下面这样:
在这里插入图片描述
我们可以把OOP的一个对象想象成一座房子,这个 '.' 就是一把钥匙,指针(或者引用)就是一群访客,它们有的拿着钥匙,有的没有拿。我还是用上面的几行代码举例。
在这里插入图片描述
第一行和第二行的 nxtp 指针,都是有钥匙的访客,通过钥匙 ('.')进入了那个结点对象的房子里,然后它们就可以为所欲为了,不仅可以修改它的next指针,甚至还能修改它的 val值,有什么他就能修改什么。所以可以通过这两个无礼的访客,就把它们所指向的那个结点(也就是那所房子)的位置改变了。

第三行nxt,它在别人家里胡作非为完了之后,出了房门,钥匙一扔,反手就变成了良民,然后它的 cur 小兄弟就赶紧招呼他,你快来,我后面有个空位!作为良民的 nxt这时两手空空的就按照给他的指引跑路了。这就是这几行代码所做的事情。

这是这道题我的一个思考,链表的题很多,经常有这种一会赋值.next一会又直接赋值给指针的操作,希望我的理解对大家能有一点帮助。

反转链表(#206)也是很经典的操作,就是要原地将链表变成逆序的. 两两交换链表中的结点 (#24) 算是反转链表的一个变形题,把翻转整个链表变成了两两交换相邻节点,这两道题虽然也是会改变首结点的位置,并且最后也需要返回首节点的地址,但是这里我们不需要用dummy结点,因为这里头结点的变化是有规律的,两个相邻节点翻转,头指针都必然会从前面的结点移动道后面的结点,所以我们可以先用一个指针提前指向即将变成头结点的结点,然后再进行翻转.

我附上它们的递归和迭代的解法,我们可以再感受一下链表操作的这个过程。
在这里插入图片描述
在这里插入图片描述
两两交换结点用递归就很合适,相当于把数组两两一组拆分,基本逻辑还是一样的,我们要先把头指针指向即将变成头结点的结点,也就是调用的递归的返回值.

在这里插入图片描述
以上就是我关于链表的一点思考, 下面我们来看位运算。

Ⅲ 位运算

关于位运算超哥已经讲得很好了,我这里还是分享两道我觉得有意思的题。第一道是比特位计数(#338)。我们先来看一下题。

在这里插入图片描述
这道题就是要求一个 [0, n] 之间所有数字的二进制形式里1的数量,我们当然可以借用求二进制1的个数的那道题里的方法,用 (i & (i - 1)) 作为一个函数求得每一个数字的结果,但是仔细一想一组连续数字的1的个数其实是有规律的,所以这道题的基本思路就是用动态规划来做。这道题确实不难,但是这道题的三种动态转移方程的思路我觉得很有意思,可以很好地帮助我们理解位运算的操作。

第一个思路,数字的奇偶性

在这里插入图片描述
通过以上几个数字,我们可以得到一个规律,就是偶数的比特位1的个数和它除以二得到的数字的比特位1的个数是相同的,比如2,4,8,它们都只有1位1,奇数的1的个数是它上一个偶数的1的个数再加上1,比如2中1的个数是1,3中1的个数就是2。由此我们就可以写出一个dp方程。

dp[i] = dp[i-1] + 1    if (i & 1) == 1
dp[i] = dp[i >> 1]     if (i & 1) == 0

第二个思路,右移

右移的本质其实就是打掉了最后一位,然后整体移过去。那么 xx >> 1的区别就是 x的最后一位是什么,如果是1,那 x 就比 x >> 1多了一位1,是 0 那就一样。我们通过 (i & 1) 来取最后一位,据此可以写出转移方程。

dp[i] = dp[i >> 1] + (i & 1);

第三个思路,将最后一位1置为0

这个思路其实很暴力,x 一定比 x 最后一位1置为0之后多了一位1,因为它少的就是我们故意置没的嘛,所以通过 (x&(x-1) 我们得到x少了最后一位1的数字,然后通过它来转移状态,状态转移方程如下:

dp[i] = dp[x & (x - 1)] + 1

这里还牵扯到了一个 (x & 1) 取最后一位的技巧,其实这个什么都可以取,要用1取最后一位是因为 1 的二进制是 0001,也就是说 1 这个数字所有位都是0,只有最后一位是1,而0 和任何数进行与运算都是0。所以假设x的二进制是 b3 b2 b1 b0 ,由于1的前面所有位都是0,所以与完之后的结果就是0 0 0 b0,只剩下最后一位 b0 要和 1 的最后一位 1 与。1和1与起来是1,1和0与起来是 0,所以如果b0最后一位是0,我们就可以得到 0,是1,我们就可以得到1。与此同理,我们要得到哪一位上的数字,就和一个只有那个位为1的数字进行与运算,我们知道只有2的幂数是只有1位1的,所以2的幂数就是我们得到某一位上数字的好帮手,比如通过与8(1000),我们就可以得到倒数第四位上的数。

最后一道题是转换成小写字母(#709),这道题也是很简单,但是延伸出来几个位运算的技巧补充给大家。

在这里插入图片描述
我们一般要用转换大小写肯定就是调库函数了,这几个位运算大概率是用不到的,但是可以很好地帮我们理解二进制和位运算。拿toLowerCase举例,我们可以通过 或 32得到,为什么?这就和我们上面说到的通过 与来取数有异曲同工之妙。

或操作的结果就是 0 | 1 = 1,1 | 1 = 1,如果是大写字母的话,对应的倒数第七位,(2^x = 32 的那位)一定是0,我们通过或操作将倒数第七位直接置为1,因为我们或的是32(0100 0000),所以不管那位本来就是1(小写字母),还是它是0(大写字母),最后的结果都是变成了1,也就是加上了32。

所以总结一下,通过 与操作& 我们可以取出某一位上的数字,通过 或操作| 我们可以将某一位上的数字置为1。

关于位运算当然不得不提N皇后,之前唐烨君前辈已经分享的很好了,我这里就再多补充一个东西,很多同学其实比较容易忽略,计算机里操作的不是原码,而是补码。我用N皇后里的一个操作来举例。
在这里插入图片描述
这一步是要把pos的最后一位1取出来,我用个比较简单的数举例,就比如我们要取 10 的最后一位1.正数的原码和补码是相同的,所以没有什么影响,10 的原码就是(0000 1010), -10 的原码就是 (1000 1010), 注意原码的最高位是符号位, 0 为 正数, 1 为负数。

现在要计算 10 的最后一位1, 就需要把 10 和 -10 相与,但是这里并不是 (0000 1010)& (1000 1010),很显然这样与出来结果是错的,得不到10的最后一位1。所以我们要先把它们转换成补码,正数的补码和原码是相同的,负数的补码要把原码除最高位符号位以外按位取反,然后末位加1。我们先对 (1000 1010) 除最高位取反,得到(1111 0101),然后末位加1,得到(1111 0110),这就是 -10 的补码,然后将这两个补码相与,(0000 1010) & (1111 0110) = (0000 0010) = 2D,这样就取出了10的最后一位1。

在这里插入图片描述
以上就是关于位运算我的一些想法,大家如果看到负数的话,一定要记得不要用它的原码去验证,一切都是补码。

关于N皇后问题的位运算解法,可以去看我的这篇题解。👉位运算 + DFS详解 (N皇后Ⅰ& N皇后Ⅱ)

这就是我今天的分享,后续我还会将各个模块的要点以及题目都整理在一起分享出来,如果能对大家有点帮助的话,我非常荣幸。

感谢大家。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值