數據結構與算法(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博客的編輯器換代了,比以前使用感受提升不止一個檔次啊哈哈哈!








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章