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