<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ?LeetCode刷題實(shí)戰(zhàn)47:全排列 II

          共 6115字,需瀏覽 13分鐘

           ·

          2020-09-24 07:53

          算法的重要性,我就不多說了吧,想去大廠,就必須要經(jīng)過基礎(chǔ)知識和業(yè)務(wù)邏輯面試+算法面試。所以,為了提高大家的算法能力,這個(gè)公眾號后續(xù)每天帶大家做一道算法題,題目就從LeetCode上面選 !

          今天和大家聊的問題叫做?全排列II,我們先來看題面:

          https://leetcode-cn.com/problems/permutations-ii/

          Given a collection of numbers that might contain duplicates, return all possible unique permutations.

          題意

          給定一個(gè)可包含重復(fù)數(shù)字的序列,返回所有不重復(fù)的全排列。

          樣例

          輸入: [1,1,2]
          輸出:
          [
          ??[1,1,2]
          ,
          ??[1,2,1],
          ??[2,1,1]
          ]

          解題

          來源:
          https://www.cnblogs.com/techflow/p/12640513.html

          無腦解決

          解決的方法有兩種,第一種是無腦解決。是的你沒有看錯(cuò),因?yàn)槲覀兎治鲆幌戮蜁l(fā)現(xiàn)next_permutation循環(huán)的方法在這題當(dāng)中仍然奏效,原因也很簡單,因?yàn)槲覀兠看斡胣ext_permutation求得字典序+1的下一個(gè)排列的時(shí)候,顯然已經(jīng)去除了重復(fù)元素的情況。為什么?因?yàn)橥瑯釉負(fù)Q位置字典序是不會變化的。
          比如下圖當(dāng)中,我們更換兩個(gè)2的順序,整個(gè)序列的字典序是沒有變化的,要使得字典序變化一定要交換不同的元素。所以這個(gè)解法是可行的。
          next_permutation是返回當(dāng)前序列字典序+1的序列的算法,這個(gè)算法在之前的文章當(dāng)中出現(xiàn)過,所以我就不贅述它的原理了。
          這個(gè)解法和上一題的最后一個(gè)解法完全一樣,連改動都不需要,我們直接把代碼copy過來把函數(shù)名匹配上就可以提交了。這也是我說這道題無腦的原因。
          class?Solution:
          ????def?get_next(self,?nums:?List[int]):
          ????????"""
          ????????Do?not?return?anything,?modify?nums?in-place?instead.
          ????????"""

          ????????#?長度
          ????????n?=?len(nums)
          ????????#?記錄圖中i-1的位置
          ????????pos?=?n?-?1
          ????????for?i?in?range(n-1,?0,?-1):
          ????????????#?如果降序破壞,說明找到了i
          ????????????if?nums[i]?>?nums[i-1]:
          ????????????????pos?=?i-1
          ????????????????break
          ????????????????
          ????????for?i?in?range(n-1,?pos,?-1):
          ????????????#?從最后開始找大于pos位置的
          ????????????if?nums[i]?>?nums[pos]:
          ????????????????#?先交換元素,在進(jìn)行翻轉(zhuǎn)
          ????????????????nums[i],?nums[pos]?=?nums[pos],?nums[i]
          ????????????????#?翻轉(zhuǎn)[pos+1,?n]區(qū)間
          ????????????????nums[pos+1:]?=?nums[n:pos:-1]
          ????????????????return?False
          ????????return?True
          ????????
          ????????
          ????def?permuteUnique(self,?nums:?List[int])?->?List[List[int]]:
          ????????ret?=?[]
          ????????#?從小到大排序,獲得最小排列
          ????????nums?=?sorted(nums)
          ????????ret.append(nums.copy())
          ????????#?如果還有下一個(gè)排列則繼續(xù)調(diào)用
          ????????while?not?self.get_next(nums):
          ????????????#?要.copy()是因?yàn)镻ython中存儲的引用,如果不加copy
          ????????????#?會導(dǎo)致當(dāng)nums發(fā)生變化之后,ret中存儲的數(shù)據(jù)也會變化
          ????????????ret.append(nums.copy())
          ????????return?ret

          回溯法

          顯然重復(fù)之前的工作并不能讓我們變得更強(qiáng),所以我們還是要回到正解上來。在之前的文章我們知道,生成全排列的通常做法是使用回溯法。那么如果使用回溯法我們怎么解決重復(fù)元素的問題呢?
          還是老慣例,我們要解決問題,首先來分析問題,我們知道重復(fù)的元素會干擾全排列生成的算法,那么它為什么會干擾,是怎么干擾的?
          在回溯法當(dāng)中,我們是順序遍歷位置,然后枚舉放置的元素。于是我們大概可以想出兩種可能導(dǎo)致重復(fù)的情況。
          我們先來看情況1,情況1很簡單,就是同一個(gè)位置放入同樣元素的情況。比如我們原數(shù)組當(dāng)中有兩個(gè)4,我們在放置的過程當(dāng)中放入第一個(gè)4和第二個(gè)4的情況是一樣的。顯然這就重復(fù)了,我們可以參考下下圖,我們給當(dāng)下所有的選擇編號,選擇2和選擇3是等價(jià)的,兩者只能選一個(gè)。
          第二種情況也比較容易想明白,還是同樣的例子,我們給數(shù)組當(dāng)中的兩個(gè)4編號,一個(gè)編號是,一個(gè)是,我們會發(fā)現(xiàn)對于兩個(gè)位置,我們先放第一個(gè)再放第二個(gè)和先放第二個(gè)再放第一個(gè)是重復(fù)的
          表面上來看情況是這兩種,但是如果深入分析會發(fā)現(xiàn)這兩種情況其實(shí)說的是一回事,結(jié)果出現(xiàn)重復(fù)都是由于全排列的時(shí)候元素出現(xiàn)不穩(wěn)定造成的。
          這里的“穩(wěn)定“其實(shí)是一個(gè)排序算法當(dāng)中的術(shù)語,我們經(jīng)常會討論某一個(gè)排序算法是穩(wěn)定的,某一個(gè)不是。這個(gè)穩(wěn)定是什么意思呢,其實(shí)就是指的元素的相對順序。比如在上面這張圖當(dāng)中的兩個(gè)4,如果在排序的結(jié)果當(dāng)中,后面一個(gè)4有可能出現(xiàn)在第一個(gè)4的前面,那么就說明這個(gè)算法是不穩(wěn)定的,同樣,如果不會出現(xiàn)這樣的情況,那么可以說這個(gè)算法是穩(wěn)定的。
          同樣,如果我們能保證執(zhí)行全排列的時(shí)候元素的穩(wěn)定性,那么這個(gè)問題就解決了。表面上看這似乎是一個(gè)優(yōu)化問題,其實(shí)不然,考察的是穩(wěn)定性這個(gè)概念。
          如果能想到穩(wěn)定性這點(diǎn),離解決已經(jīng)很近了。我們要保證穩(wěn)定性,也就是說對于當(dāng)前元素,我們需要保證前面的同元素已經(jīng)放置了,那么在一個(gè)排列中,相同元素的擺放位置應(yīng)該是遞增的。我們用map記錄每一個(gè)元素最后一次擺放的位置,控制它在往下遞歸的過程當(dāng)中遞增即可。
          比如當(dāng)數(shù)組當(dāng)中有兩個(gè)4的時(shí)候,第一個(gè)4如果還沒有擺放,那么第二個(gè)4也不能擺。但是由于我們在判斷要不要擺放第二個(gè)4的時(shí)候,并不知道前面是否還有其他的4,所以我們只能倒過來,判斷在擺放第一個(gè)4的時(shí)候,是不是已經(jīng)放過了后面的4,如果是的話,那么這個(gè)操作不能執(zhí)行。用語言描述這個(gè)邏輯有點(diǎn)繞,我們來看下圖就明白了:
          我們擺放了第一個(gè)4之后,map[4] = 4,記錄的是擺放的4的下標(biāo),當(dāng)我們枚舉放置第一個(gè)4的時(shí)候,發(fā)現(xiàn)已經(jīng)放置的4下標(biāo)大于當(dāng)前,說明非法,放置了會引起重復(fù)。
          還有一個(gè)問題是當(dāng)我們回溯的時(shí)候,需要重置map里的值,比如:
          在一次遞歸當(dāng)中,我們放置了兩個(gè)4,放了第二個(gè)4之后,map[4] = 4,當(dāng)我們回溯彈出第二個(gè)4的時(shí)候,這個(gè)時(shí)候的map[4]應(yīng)該是多少?
          答案不難,應(yīng)該是1,也就是第一個(gè)4的下標(biāo)。所以我們在遞歸的時(shí)候,在更新map之前,需要記錄一下之前的值,方便回溯的時(shí)候恢復(fù)。
          我們整理一下,思路可以得到代碼:
          class?Solution:
          ????def?dfs(self,?nums,?n,?i,?states,?cur,?ret,?flag):
          ????????if?i?==?n:
          ????????????ret.append(cur.copy())
          ????????????return
          ????????for?p?in?range(n):????????????????
          ????????????#?遍歷所有元素
          ????????????#?如果p元素已經(jīng)放置過了,或者是已經(jīng)放置了更后面的同元素
          ????????????#?跳過
          ????????????if?flag[p]?or?(nums[p]?in?states?and?states[nums[p]]?>=?p):
          ????????????????continue
          ????????????#?當(dāng)前位置放置p
          ????????????cur.append(nums[p])
          ????????????#?更新之前,記錄之前的值
          ????????????tmp,?states[nums[p]]?=?states[nums[p]]?if?nums[p]?in?states?else?-1,?p
          ????????????#?flag[p]置為True
          ????????????flag[p]?=?True
          ????????????#?遞歸
          ????????????self.dfs(nums,?n,?i+1,?states,?cur,?ret,?flag)
          ????????????#?回溯
          ????????????cur.pop()
          ????????????flag[p]?=?False
          ????????????#?恢復(fù)遞歸之前的值
          ????????????states[nums[p]]?=?tmp?
          ????????????#?states.remove((i,?nums[p]))
          ????????
          ????????
          ????def?permuteUnique(self,?nums:?List[int])?->?List[List[int]]:
          ????????ret?=?[]
          ????????n?=?len(nums)
          ????????#?記錄元素i有沒有放置過
          ????????flag?=?[False?for?_?in?range(n)]
          ????????self.dfs(nums,?n,?0,?{},?[],?ret,?flag)
          ????????return?ret

          改進(jìn)

          上面這個(gè)方法其實(shí)有點(diǎn)復(fù)雜,理解起來有很多細(xì)節(jié),一個(gè)比較蛋疼的點(diǎn)就是我們用了map去記錄了位置,由于要更新map,所以還需要記錄之前的值,還需要判斷元素不在map當(dāng)中的情況。并且map的查找存在開銷,那么我們能不能不用map呢?
          在想怎么不用map的替代方案之前,我們需要先搞清楚,我們?yōu)槭裁匆褂胢ap?
          這個(gè)問題問的并不是一個(gè)充分問題,而是一個(gè)必要問題,不是用map解決了什么問題,而是我們?yōu)槭裁粗荒苡胢ap不可呢?
          因?yàn)閙ap是一個(gè)容器,它能存儲數(shù)據(jù)。對于一個(gè)元素t來說,我們并不知道它在數(shù)組nums當(dāng)中之前出現(xiàn)的位置是哪里,我們也并不知道這些之前出現(xiàn)的t有沒有被放入當(dāng)前的排列里。我們用map記錄下t的下標(biāo),本質(zhì)原因是這個(gè)。map只是我們實(shí)現(xiàn)這個(gè)功能的手段,不是目的。
          所以我們?nèi)绻幌胧褂胢ap,必須要保證能夠知道每一個(gè)元素的擺放位置才可以。要做到這點(diǎn)其實(shí)并不難,我們只需要對nums排序就好了。排序之后,所有相同的元素都會挨在一起。那么,對于位置p來說,我們只需要判斷如果nums[p-1] 等于 nums[p]的話,必須要flag[p-1]等于true,也就是說對于元素v來說,它前面一位如果是v必須要放置好,才能放置當(dāng)前的v,否則就是非法的。這樣每一個(gè)v都限制前一個(gè)v,就保證了所有的v不會起沖突。這是一種鏈?zhǔn)较拗?/strong>的思想。
          通過這種方法,我們就可以拋棄掉map的使用,進(jìn)而極大地提升效率。
          想清楚了原理之后,代碼非常簡單:
          class?Solution:
          ????def?dfs(self,?nums,?n,?i,?cur,?ret,?flag):
          ????????if?i?==?n:
          ????????????ret.append(cur.copy())
          ????????????return
          ????????for?p?in?range(n):????????????????
          ????????????#?遍歷所有元素
          ????????????#?如果p元素已經(jīng)放置過了,或者
          ????????????#?如果nums[p-1]?==?nums[p]?并且flag[p-1]?是False
          ????????????#?跳過
          ????????????if?flag[p]?or?(p?>?0?and?nums[p-1]?==?nums[p]?and?not?flag[p-1]):
          ????????????????continue
          ????????????#?當(dāng)前位置放置p
          ????????????cur.append(nums[p])
          ????????????#?flag[p]置為True
          ????????????flag[p]?=?True
          ????????????#?遞歸
          ????????????self.dfs(nums,?n,?i+1,?cur,?ret,?flag)
          ????????????#?回溯
          ????????????cur.pop()
          ????????????flag[p]?=?False
          ????????
          ????????
          ????def?permuteUnique(self,?nums:?List[int])?->?List[List[int]]:
          ????????ret?=?[]
          ????????n?=?len(nums)
          ????????#?記錄元素i有沒有放置過
          ????????nums?=?sorted(nums)
          ????????flag?=?[False?for?_?in?range(n)]
          ????????self.dfs(nums,?n,?0,?[],?ret,?flag)
          ????????return?ret
          你看,我們只需要排序,也不需要引入新的數(shù)據(jù)結(jié)構(gòu),就可以完美地解決這個(gè)問題。其實(shí)很多時(shí)候,解決問題的途徑有很多種,能不能想到更好的解法除了取決于能力之外,更重要的還是對問題的理解。
          這道題也是一個(gè)Medium的問題,總體來說難度并不大。如果可以不看標(biāo)程,獨(dú)立通過這道題,就說明對全排列這個(gè)問題的思考已經(jīng)比較到位了。
          好了,今天的文章就到這里,如果覺得有所收獲,請順手點(diǎn)個(gè)在看或者轉(zhuǎn)發(fā)吧,你們的支持是我最大的動力。


          上期推文:


          LeetCode1-20題匯總,速度收藏!
          LeetCode21-40題匯總,速度收藏!
          LeetCode刷題實(shí)戰(zhàn)40:組合總和 II
          LeetCode刷題實(shí)戰(zhàn)41:缺失的第一個(gè)正數(shù)
          LeetCode刷題實(shí)戰(zhàn)42:接雨水
          LeetCode刷題實(shí)戰(zhàn)43:字符串相乘
          LeetCode刷題實(shí)戰(zhàn)44:通配符匹配
          LeetCode刷題實(shí)戰(zhàn)45:跳躍游戲 II
          LeetCode刷題實(shí)戰(zhàn)46:全排列


          瀏覽 41
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩黄片免费看 | 人妻爽爽人妻夜夜 | 91青青草 | 影视先锋男人资源网 | 日日拍夜夜拍 |