数据结构与算法(1)链表,基于Python解决几个简单的面试题

最近头一直很大,老板不停地布置各种任务,根本没有时间干自己的事情,真的好想鼓起勇气和他说,我以后不想干这个了,我文章也发了您就让我安安稳稳混到毕业行不行啊……

作为我们这些想要跨专业的人来说,其实很大的一个劣势就是没有经历过一个计算机学科完整的培养,所以对计算机专业的一些很基本但又很重要的内容缺乏足够的了解,比如,数据结构与算法。我们日常做科研其实写代码也挺多的,一开始我也觉得虽然我不懂数据结构但好像也不影响我实现我的功能啊,但后来我慢慢就发现,那样写的程序缺点很多

1.     复用性很差,比如某个模型只是换了几个参数,那我得在整个代码找到所有与这些参数相关的部分进行修改,非常麻烦,但如果你一开始抽象了一个非常好的数据结构来描述你的模型,那你只需要在定义的时候修改一下就行了,这样效率确实高很多。

2.     代码很冗长,因为很多操作其实在一个程序里是会反复用到了。一开始写一些计算程序的时候我非常享受代码写很长,给自己一种很厉害的错觉,其实现在回过头来看看,很多都只是同样的操作,只是换了不同的对象而已,既然这种操作这么频繁,为什么不把它抽象出来,这样又清晰又简洁。

等等等等,不一而足。当然这只是我自己的感受,有的地方可能描述地也不那么准确,但接下来这点原因肯定值得我的足够重视,那就是几乎所有的公司面试或笔试都会考核数据结构与算法。或许有的人会说,我要做算法工程师,不是去做开发,但算法工程师,那你也得先做个工程师啊,所以啥也别说了,好好学吧!

今天第一部分打算写链表,这也算是比较简单的部分,当做是练练手吧。为了配合自己的学习,在网上买了一个网课讲算法的,不过实现都是C/C++,刚好把里面举例的问题都用Python实现一遍锻炼一下自己。

首先讲一下链表的定义和实现,熟悉的同学直接跳过就行去看下一部分就行了链表是线性表的一种,所谓线性表又两个要求:

1 你能找到这个表的首元素。

2 从表里的任意元素出发可以找到它的下一个元素。

那么显而易见,最简单的实现方法就是顺序表,在内存中申请一块固定的空间用以存放每个元素的数据或者地址,这样的好处是查找的效率非常高,常数时间的复杂度就可以完成,但也面临问题,就是假如这个表的大小没确定,申请多少空间呢?这里就有一个闲暇空间和申请新空间频率之间的一个平衡。而链表就不存在这样的问题了,它的每个元素存储的地址是离散的,我只要知道当前元素的值和它下一个元素的地址就ok,下面我们就详细讨论一个单链表的实现过程。

首先我们得定义一个节点类,用于表示链表中的每个元素,那么很明显,它应该有两个属性,当前元素的值和它下一个元素的地址,实现也很简单

class Lnode():
    def __init__(self,elem,next_=None):
        self.elem=elem
        self.next=next_

接下来,我们就得考虑链表所需要进行的各种操作。

1 初始化创建空表。

通常的做法是构造一个表头元素并将其elem赋值为None,其next也赋值成None。当然也可以只用一个指针指向链表中的第一个元素,这样做的缺点就是下面写那些操作函数的时候总要把在表头处的操作单独拎出来讨论,而头结点用一个空LNode就可以避免了这个问题,所以为了方便咱还是这么做吧。

2 删除链表

在Python里很方便不用去一个一个释放链表中所有的元素,直接将头结点next赋值为None即可,原来链表的节点会由解释器去处理。

3 判断链表是否为空

还记得我们的表头元素吗,根据其next是否是None来判断链表是否为空就行。

4 插入元素

链表就是一系列连在一起的元素,所以当我们想要插入某个元素的时候,肯定得把某个链子打开,那这样就会涉及到这个链子之前连接的两个元素,我们把链子前面那个元素称为pre,那么后面那个元素就是pre.next,现在我们要做的第一步是把要插入的元素指向pre.next,然后再把pre的next指向当前元素,这两个操作顺序不能相反,为啥呢,你先修改pre的next我们就把链表后面的部分给丢了啊……

5 删除元素

想象一个链子中间要拿掉某个元素,那我们是不是要把之前和这个元素相连的两个链子给连起来呢,其实也就是修改pre的next将其指向pre.next.next。

6 查找

单链表的查找其实就涉及链表的遍历,我们只有从表头的next开始,依次指向其next元素直到发现满足要求或者尾元素为止。

下面就是一个Python中链表的简单实现。

class LinkedList():
    def __init__(self):
        self._head=Lnode(None)
        
    def is_empty(self):
        return self._head.next is None
        
    def prepend(self,elem):
        self._head.next=Lnode(elem,self._head.next)
        
    def append(self,elem):
        p=self._head
        while (p.next is not None):
            p=p.next
        p.next=Lnode(elem)
        
    def insert(self,elem,i):
        if i<0 or not isinstance(i,int):
            raise ValueError('Invalid index')
        else:
            index=0
            p=self._head
            while p is not None:
                if index==i:
                    p.next=Lnode(elem,p.next)
                    break
                else:
                    p=p.next
                    index+=1
                
    def pop(self):
        if self._head.next is None:
            raise ValueError('No element to pop')
        else:
            e=self._head.next.elem
            self._head.next=self._head.next.next
            return e
            
    def find(self,elem):
        p=self._head
        index=0
        while p is not None:
            if p.next.elem==elem:
                return index
            else:
                p=p.next
                index+=1
        return 'Not find'
                
    def __str__(self):
        p=self._head
        temp=''
        while p.next is not None:
            temp+=str(p.next.elem)
            temp+='->'
            p=p.next
        temp+='None'
        return temp

基于上面的定义我们做一个简单的测试

#Test
l1=LinkedList()
l1.prepend(1)
l1.prepend(2)
print l1
l1.append(3)
l1.append(4)
print l1
l1.insert(5,1)
print l1
l1.pop()
print l1
print l1.find(5)

结果如下

2->1->None

2->1->3->4->None

2->5->1->3->4->None

5->1->3->4->None

0

这里我们基本实现了一个链表,当然还有一些功能后面我们有需要再去写,比如删除指定元素等等,然后还要注意的一个部分就是一些异常情况的判定,比如不合法的输入等等,我们这里就不深究了,接下来我们主要是解决几个关于链表的实际问题。

===============================================================================

1 链表相加

用1->2->3表示321,2->3->1表示132,那两者相加应该是453,即3->5->4,即用链表完成竖式加法。仔细一想,这还确实挺合适链表来做的,因为从首元素开始弹出刚好是从低位开始的。过程中需要注意的两个地方,一个是要考虑两个链表位数不同的情况,即其中某一个链表到头之后,要将另外一个长链表迭代到底,还有一个特殊情况就是到了最后一位进位不为0,我们需要再补一位,具体实现如下

def ll__add(l1,l2):
    res=LinkedList()
    carry=0
    p1=l1._head.next
    p2=l2._head.next
    while (p1 is not None and p2 is not None):
        value=p1.elem+p2.elem+carry
        carry=value/10
        value=value%10
        res.append(value)
        p1=p1.next
        p2=p2.next
    if p1 is not None:
        temp=p1
    else:
        temp=p2
    while(temp is not None):
        value=temp.elem+carry
        carry=value/10
        value=value%10
        res.append(value)
        temp=temp.next
    if carry!=0:
        res.append(carry)
    return res

可以用一个简单的例子进行测试

l1=LinkedList()
l2=LinkedList()
for i in range(5):
    l1.prepend(randint(0,9))
for i in range(8):
    l2.prepend(randint(0,9))
print l1
print l2
print ll__add(l1,l2)

结果如下

6->5->6->7->8->None

0->7->0->7->1->6->9->3->None

6->2->7->4->0->7->9->3->None

确实是达到了我们的要求的。

===========================================================================

2 链表部分翻转

所谓链表的部分翻转,就是我指定一个起始和重点位置,将这个区域内所有的元素翻转。其实这个问题思路野蛮明确的,首先我得找到这一整个区域的前一个节点,因为它相当于是这个区域和外部的接口,然后我们从这个区域的第二个元素开始,将每个元素依次移动到前面那个接口的后面,这样整个区域走完后也就达到了翻转的目的。那再考虑后面这个翻转的操作,肯定需要指针指向每次操作的那个元素,还需要一个其前面元素的指针,因为该元素移走后我们得把后面的链子继续接上啊,但因为是从区域内第二个元素开始的,所以我们发现每次前面那个元素就是翻转区域内的第一个元素。好了,到这里大概清楚了,我们一共需要三个指针,翻转区域前那个元素,翻转区域第一个元素,当前操作元素,每次操作我们先将翻转区域第一个元素指向当前操作元素的下一个元素,再把操作元素插入到翻转区域第一个位置,最后再更新操作元素即可,实现方式如下

#Reverse
def reverse(ll,start,end):
    index=0
    p1=ll._head
    while index<start:
        p1=p1.next
        index+=1
    p2=p1.next.next
    p3=p1.next
    index+=1
    while index<=end:
        tmp=p2.next
        p2.next=p1.next
        p1.next=p2
        p3.next=tmp
        p2=tmp
        index+=1
        
    return ll

用一个简单的例子测试一下

l1=LinkedList()
for i in range(10):
    l1.prepend(randint(0,9))
print l1
print reverse(l1,0,5)
print reverse(l1,0,9)

结果如下

9->0->4->7->3->0->7->5->6->5->None

0->3->7->4->0->9->7->5->6->5->None

5->6->5->7->9->0->4->7->3->0->None

可以说是非常OK了。

3 排序链表去重

就是给定排序好的链表,如果中间出现重复的元素只保留一个。比如说5->5->4->3->3->2->1->1->1->0->None,去重后就只剩下5->4->3->2->1->0->None了。这一题还是比较简单的,依次处理链表中的元素,前后两个元素值不相同则一起后移,如果相同则把后面重复的那个元素从链表中去除,Python中的实现如下

def delduplicate(ll):
    cur=ll._head.next
    pre=ll._head.next
    while pre is not None:
        cur=pre.next
        if cur==None:
            break
        if cur.elem==pre.elem:
            pre.next=cur.next
        else:
            pre=cur
    return ll

代码确实很短啊,我们就用上面那个例子做一个简单的测试

l1=LinkedList()
for i in [0,1,1,1,2,3,3,4,5,5]:
    l1.prepend(i)
print l1
print delduplicate(l1)

输出如下

5->5->4->3->3->2->1->1->1->0->None

5->4->3->2->1->0->None

没有问题,下一个,哈哈哈。

4 链表划分

链表划分就是说给定一个阈值,小于该阈值统统移动到链表前端,大于该阈值的则移动到列表后端,然后链表要求保序。这个问题如果想在链表上就地操作其实也可以,不过这样需要一个指针始终指向小于阈值链表部分的尾端,再用一个指针再整个链表上进行迭代就行了。这里我们采用一个更简洁的办法,就是直接申请两个新的链表,小于阈值的进一个,大于阈值的进另一个,最后将两个链表相连即可,Python中的实现如下

def partition(ll,x):
    p=ll._head.next
    l1=LinkedList()
    l2=LinkedList()
    p1=l1._head
    p2=l2._head
    while(p is not None):
        
        if p.elem<=x:
            p1.next=p
            p1=p
        else:
            p2.next=p
            p2=p
        p=p.next
    p2.next=None
    p1.next=l2._head.next
    return l1

老规矩,还是用一个例子来测试一下

l1=LinkedList()
for i in range(10):
    l1.prepend(randint(0,9))
print l1
print partition(l1,5)

输出结果如下

5->3->0->3->8->9->5->3->0->0->None

5->3->0->3->5->3->0->0->8->9->None

好的,链表部分就到这里,说实话现在正是找实习的时候,我才看到链表还来得及吗……

加油加油!!!

PS:好久没写博客发现CSDN博客的编辑器换代了,比以前使用感受提升不止一个档次啊哈哈哈!

    原文作者:王大宝的CD
    原文地址: https://blog.csdn.net/sinat_22594309/article/details/79568475
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞