Cara menemukan dan mengembalikan nilai duplikat dalam array

170

arr adalah array dari string:

["hello", "world", "stack", "overflow", "hello", "again"]

Apa cara yang mudah dan elegan untuk memeriksa apakah arrmemiliki duplikat, dan jika demikian, kembalikan salah satu dari mereka (tidak peduli yang mana)?

Contoh:

["A", "B", "C", "B", "A"]    # => "A" or "B"
["A", "B", "C"]              # => nil
Misha Moroshko
sumber
arr == arr.uniqakan menjadi cara yang mudah dan elegan untuk memeriksa apakah arrmemiliki duplikat, namun, itu tidak memberikan yang digandakan.
Joel AZEMAR

Jawaban:

249
a = ["A", "B", "C", "B", "A"]
a.detect{ |e| a.count(e) > 1 }

Saya tahu ini bukan jawaban yang sangat elegan, tetapi saya menyukainya. Kode satu liner itu indah. Dan berfungsi dengan baik kecuali Anda perlu memproses kumpulan data besar.

Mencari solusi yang lebih cepat? Ini dia!

def find_one_using_hash_map(array)
  map = {}
  dup = nil
  array.each do |v|
    map[v] = (map[v] || 0 ) + 1

    if map[v] > 1
      dup = v
      break
    end
  end

  return dup
end

Itu linear, O (n), tetapi sekarang perlu mengelola beberapa baris kode, perlu kasus uji, dll.

Jika Anda membutuhkan solusi yang lebih cepat, mungkin coba C sebagai gantinya.

Dan inilah intisari yang membandingkan berbagai solusi: https://gist.github.com/naveed-ahmad/8f0b926ffccf5fbd206a1cc58ce9743e

Naveed
sumber
59
Kecuali kuadrat untuk sesuatu yang bisa diselesaikan dalam waktu linier.
jasonmp85
18
Memberikan solusi O (n ^ 2) untuk masalah linier bukanlah cara yang tepat.
tdgs
21
@ jasonmp85 - Benar; Namun, itu hanya mempertimbangkan runtime besar-O. dalam praktiknya, kecuali jika Anda menulis kode ini untuk beberapa data skala besar (dan jika demikian, Anda sebenarnya bisa menggunakan C atau Python), jawaban yang diberikan jauh lebih elegan / mudah dibaca, dan tidak akan berjalan lebih lambat dibandingkan untuk solusi waktu linier. lebih jauh lagi, dalam teori, solusi waktu linier membutuhkan ruang linier, yang mungkin tidak tersedia
David T.
26
@Kalanamith Anda bisa mendapatkan nilai duplikat menggunakan inia.select {|e| a.count(e) > 1}.uniq
Naveed
26
Masalah dengan metode "deteksi" adalah berhenti ketika menemukan duplikat pertama, dan tidak memberikan Anda semua dups.
Jaime Bellmyer
214

Anda dapat melakukan ini dalam beberapa cara, dengan opsi pertama menjadi yang tercepat:

ary = ["A", "B", "C", "B", "A"]

ary.group_by{ |e| e }.select { |k, v| v.size > 1 }.map(&:first)

ary.sort.chunk{ |e| e }.select { |e, chunk| chunk.size > 1 }.map(&:first)

Dan opsi O (N ^ 2) (yaitu kurang efisien):

ary.select{ |e| ary.count(e) > 1 }.uniq
Ryan LeCompte
sumber
17
Dua yang pertama jauh lebih efisien untuk array besar. Yang terakhir adalah O (n * n) sehingga bisa lambat. Saya perlu menggunakan ini untuk array dengan ~ elemen 20k dan dua yang pertama kembali hampir seketika. Saya harus membatalkan yang ketiga karena butuh waktu lama. Terima kasih!!
Venkat D.
5
Hanya sebuah pengamatan tetapi dua yang pertama yang diakhiri dengan .map (&: pertama) bisa diakhiri dengan .key karena bagian itu hanya menarik kunci pada hash.
engineerDave
@engineerDave itu tergantung pada versi ruby ​​yang digunakan. 1.8.7 membutuhkan &: pertama atau bahkan {| k, _ | k} tanpa ActiveSupport.
Emirikol
berikut adalah beberapa tolok ukur gist.github.com/equivalent/3c9a4c9d07fff79062a3 dalam kinerja, pemenangnya jelas group_by.select
setara
6
Jika Anda menggunakan Ruby> 2.1, Anda dapat menggunakan: ary.group_by(&:itself). :-)
Drenmi
44

Cukup temukan contoh pertama di mana indeks objek (dihitung dari kiri) tidak sama dengan indeks objek (dihitung dari kanan).

arr.detect {|e| arr.rindex(e) != arr.index(e) }

Jika tidak ada duplikat, nilai kembali akan menjadi nol.

Saya percaya ini adalah solusi tercepat yang diposting di utas sejauh ini, juga, karena tidak bergantung pada pembuatan objek tambahan, dan #indexdan #rindexdiimplementasikan dalam C. Runtime besar-O adalah N ^ 2 dan dengan demikian lebih lambat daripada Sergio, tetapi waktu dinding bisa lebih cepat karena fakta bahwa bagian "lambat" berjalan di C.

Chris Heald
sumber
5
Saya suka solusi ini, tetapi hanya akan mengembalikan duplikat pertama. Untuk menemukan semua duplikat:arr.find_all {|e| arr.rindex(e) != arr.index(e) }.uniq
Josh
1
Jawaban Anda juga tidak menunjukkan cara menemukan apakah ada rangkap tiga, atau apakah seseorang dapat menggambar elemen dari array untuk mengeja "CAT".
Cary Swoveland
3
@ bruno077 Bagaimana waktu linier ini?
beauby
4
@ Chris Great jawaban, tapi saya pikir Anda bisa melakukan sedikit lebih baik dengan ini: arr.detect.with_index { |e, idx| idx != arr.rindex(e) }. Penggunaan with_indexharus menghapus keharusan untuk indexpencarian pertama .
ki4jnq
Bagaimana Anda mengadaptasi ini ke array 2D, membandingkan duplikat dalam kolom?
ahnbizcad
30

detecthanya menemukan satu duplikat. find_allakan menemukan semuanya:

a = ["A", "B", "C", "B", "A"]
a.find_all { |e| a.count(e) > 1 }
JjP
sumber
3
Pertanyaannya sangat spesifik sehingga hanya satu duplikat yang akan dikembalikan. Imo, menunjukkan cara menemukan semua duplikat baik-baik saja, tetapi hanya sebagai tambahan untuk jawaban yang menjawab pertanyaan yang diajukan, yang belum Anda lakukan. btw, sangat tidak efisien untuk memanggil countsetiap elemen dalam array. (A penghitungan hash, misalnya, jauh lebih efisien, misalnya, membangun h = {"A"=>2, "B"=>2, "C"=> 1 }kemudian h.select { |k,v| v > 1 }.keys #=> ["A", "B"].
Cary Swoveland
24

Berikut adalah dua cara lain untuk menemukan duplikat.

Gunakan satu set

require 'set'

def find_a_dup_using_set(arr)
  s = Set.new
  arr.find { |e| !s.add?(e) }
end

find_a_dup_using_set arr
  #=> "hello" 

Gunakan selectsebagai pengganti findarray semua duplikat.

Menggunakan Array#difference

class Array
  def difference(other)
    h = other.each_with_object(Hash.new(0)) { |e,h| h[e] += 1 }
    reject { |e| h[e] > 0 && h[e] -= 1 }
  end
end

def find_a_dup_using_difference(arr)
  arr.difference(arr.uniq).first
end

find_a_dup_using_difference arr
  #=> "hello" 

Penurunan .first untuk mengembalikan larik semua duplikat.

Kedua metode kembali nil jika tidak ada duplikat.

Saya mengusulkan agarArray#difference ditambahkan ke inti Ruby. Informasi lebih lanjut ada dalam jawaban saya di sini .

Tolok ukur

Mari kita bandingkan metode yang disarankan. Pertama, kita membutuhkan array untuk pengujian:

CAPS = ('AAA'..'ZZZ').to_a.first(10_000)
def test_array(nelements, ndups)
  arr = CAPS[0, nelements-ndups]
  arr = arr.concat(arr[0,ndups]).shuffle
end

dan metode untuk menjalankan benchmark untuk berbagai test array:

require 'fruity'

def benchmark(nelements, ndups)
  arr = test_array nelements, ndups
  puts "\n#{ndups} duplicates\n"    
  compare(
    Naveed:    -> {arr.detect{|e| arr.count(e) > 1}},
    Sergio:    -> {(arr.inject(Hash.new(0)) {|h,e| h[e] += 1; h}.find {|k,v| v > 1} ||
                     [nil]).first },
    Ryan:      -> {(arr.group_by{|e| e}.find {|k,v| v.size > 1} ||
                     [nil]).first},
    Chris:     -> {arr.detect {|e| arr.rindex(e) != arr.index(e)} },
    Cary_set:  -> {find_a_dup_using_set(arr)},
    Cary_diff: -> {find_a_dup_using_difference(arr)}
  )
end

Saya tidak memasukkan jawaban @ JjP karena hanya satu duplikat yang akan dikembalikan, dan ketika jawabannya diubah untuk melakukan itu sama dengan jawaban @ Naveed sebelumnya. Saya juga tidak memasukkan jawaban @ Marin, yang, ketika diposting sebelum jawaban @ Naveed, mengembalikan semua duplikat daripada hanya satu (titik kecil tetapi tidak ada gunanya mengevaluasi keduanya, karena mereka identik ketika mengembalikan hanya satu duplikat).

Saya juga memodifikasi jawaban lain yang mengembalikan semua duplikat untuk mengembalikan hanya yang pertama ditemukan, tetapi yang seharusnya tidak berpengaruh pada kinerja, karena mereka menghitung semua duplikat sebelum memilih satu.

Hasil untuk setiap tolok ukur terdaftar dari yang tercepat hingga yang paling lambat:

Pertama anggap array berisi 100 elemen:

benchmark(100, 0)
0 duplicates
Running each test 64 times. Test will take about 2 seconds.
Cary_set is similar to Cary_diff
Cary_diff is similar to Ryan
Ryan is similar to Sergio
Sergio is faster than Chris by 4x ± 1.0
Chris is faster than Naveed by 2x ± 1.0

benchmark(100, 1)
1 duplicates
Running each test 128 times. Test will take about 2 seconds.
Cary_set is similar to Cary_diff
Cary_diff is faster than Ryan by 2x ± 1.0
Ryan is similar to Sergio
Sergio is faster than Chris by 2x ± 1.0
Chris is faster than Naveed by 2x ± 1.0

benchmark(100, 10)
10 duplicates
Running each test 1024 times. Test will take about 3 seconds.
Chris is faster than Naveed by 2x ± 1.0
Naveed is faster than Cary_diff by 2x ± 1.0 (results differ: AAC vs AAF)
Cary_diff is similar to Cary_set
Cary_set is faster than Sergio by 3x ± 1.0 (results differ: AAF vs AAC)
Sergio is similar to Ryan

Sekarang pertimbangkan sebuah array dengan 10.000 elemen:

benchmark(10000, 0)
0 duplicates
Running each test once. Test will take about 4 minutes.
Ryan is similar to Sergio
Sergio is similar to Cary_set
Cary_set is similar to Cary_diff
Cary_diff is faster than Chris by 400x ± 100.0
Chris is faster than Naveed by 3x ± 0.1

benchmark(10000, 1)
1 duplicates
Running each test once. Test will take about 1 second.
Cary_set is similar to Cary_diff
Cary_diff is similar to Sergio
Sergio is similar to Ryan
Ryan is faster than Chris by 2x ± 1.0
Chris is faster than Naveed by 2x ± 1.0

benchmark(10000, 10)
10 duplicates
Running each test once. Test will take about 11 seconds.
Cary_set is similar to Cary_diff
Cary_diff is faster than Sergio by 3x ± 1.0 (results differ: AAE vs AAA)
Sergio is similar to Ryan
Ryan is faster than Chris by 20x ± 10.0
Chris is faster than Naveed by 3x ± 1.0

benchmark(10000, 100)
100 duplicates
Cary_set is similar to Cary_diff
Cary_diff is faster than Sergio by 11x ± 10.0 (results differ: ADG vs ACL)
Sergio is similar to Ryan
Ryan is similar to Chris
Chris is faster than Naveed by 3x ± 1.0

Catatan yang find_a_dup_using_difference(arr)akan jauh lebih efisien jika Array#differencediimplementasikan dalam C, yang akan terjadi jika ditambahkan ke inti Ruby.

Kesimpulan

Banyak jawaban yang masuk akal tetapi menggunakan Set adalah pilihan terbaik yang jelas . Ini tercepat dalam kasus-kasus menengah-keras, paling cepat bersama dalam yang paling sulit dan hanya dalam kasus-kasus sepele komputasi - ketika pilihan Anda tidak masalah lagi - dapat dikalahkan.

Satu kasus yang sangat istimewa di mana Anda dapat memilih solusi Chris adalah jika Anda ingin menggunakan metode ini untuk secara terpisah menduplikasi duplikat ribuan array kecil dan berharap menemukan duplikat yang biasanya kurang dari 10 item. Ini akan menjadi sedikit lebih cepat karena menghindari overhead tambahan kecil untuk membuat Set.

Cary Swoveland
sumber
1
Solusi yang sangat baik. Tidak begitu jelas apa yang terjadi pada awalnya sebagai beberapa metode, tetapi harus berjalan dalam waktu yang benar-benar linier, dengan mengorbankan sedikit memori.
Chris Heald
Dengan find_a_dup_using_set, saya mendapatkan Set kembali, bukan salah satu duplikat. Juga saya tidak dapat menemukan "find.with_object" di Ruby docs dimanapun.
ScottJ
@Scottj, terima kasih atas tangkapannya! Sangat menarik bahwa tidak ada yang menangkap itu sebelumnya. Aku telah memperbaikinya. Itu Enumerable # find dirantai ke Enumerator # with_object . Saya akan memperbarui tolok ukur, menambahkan solusi Anda dan lainnya.
Cary Swoveland
1
Perbandingan sempurna @CarySwoveland
Naveed
19

Sayangnya sebagian besar jawabannya adalah O(n^2).

Ini O(n)solusinya,

a = %w{the quick brown fox jumps over the lazy dog}
h = Hash.new(0)
a.find { |each| (h[each] += 1) == 2 } # => 'the"

Apa kerumitan ini?

  • Berlari O(n) dan istirahat pada pertandingan pertama
  • Menggunakan O(n)memori, tetapi hanya jumlah minimal

Sekarang, tergantung pada seberapa sering duplikat dalam array Anda runtime ini mungkin sebenarnya menjadi lebih baik. Sebagai contoh jika array ukuran O(n)telah diambil sampel dari populasi k << nelemen yang berbeda hanya kompleksitas untuk runtime dan ruang menjadi O(k), namun lebih mungkin bahwa poster asli memvalidasi input dan ingin memastikan tidak ada duplikat. Dalam hal ini baik runtime maupun kompleksitas memori O(n)karena kami memperkirakan elemen tidak memiliki pengulangan untuk sebagian besar input.

akuhn
sumber
15

Objek Ruby Array memiliki metode yang hebat select,.

select {|item| block }  new_ary
select  an_enumerator

Bentuk pertama adalah minat Anda di sini. Ini memungkinkan Anda untuk memilih objek yang lulus tes.

Objek Ruby Array memiliki metode lain count,.

count  int
count(obj)  int
count { |item| block }  int

Dalam hal ini, Anda tertarik pada duplikat (objek yang muncul lebih dari satu kali dalam array). Tes yang sesuai adalah a.count(obj) > 1.

Jika a = ["A", "B", "C", "B", "A"]demikian

a.select{|item| a.count(item) > 1}.uniq
=> ["A", "B"]

Anda menyatakan bahwa Anda hanya menginginkan satu objek. Jadi pilih satu.

Martin Velez
sumber
1
Saya suka yang satu ini, tetapi Anda harus melempar uniq di akhir atau Anda akan mendapatkannya["A", "B", "B", "A"]
Joeyjoejoejr
1
Jawaban yang bagus Ini persis apa yang saya cari. Seperti yang ditunjukkan oleh @ Joeyjoejoejr. Saya telah mengirimkan hasil edit untuk dimasukkan ke .uniqdalam array.
Surya
Ini sangat tidak efisien. Anda tidak hanya menemukan semua duplikat dan kemudian membuang semua kecuali satu, Anda memohon countuntuk setiap elemen array, yang boros dan tidak perlu. Lihat komentar saya pada jawaban JjP.
Cary Swoveland
Terima kasih telah menjalankan tolok ukur. Sangat berguna untuk melihat bagaimana berbagai solusi dibandingkan dalam menjalankan waktu. Jawaban elegan dapat dibaca tetapi seringkali bukan yang paling efisien.
Martin Velez
9

find_all () mengembalikan sebuah arraymengandung semua elemen enumyang blocktidak false.

Untuk mendapatkan duplicateelemen

>> arr = ["A", "B", "C", "B", "A"]
>> arr.find_all { |x| arr.count(x) > 1 }

=> ["A", "B", "B", "A"]

Atau uniqelemen rangkap

>> arr.find_all { |x| arr.count(x) > 1 }.uniq
=> ["A", "B"] 
Rokibul Hasan
sumber
7

Sesuatu seperti ini akan berhasil

arr = ["A", "B", "C", "B", "A"]
arr.inject(Hash.new(0)) { |h,e| h[e] += 1; h }.
    select { |k,v| v > 1 }.
    collect { |x| x.first }

Artinya, letakkan semua nilai ke hash di mana kunci adalah elemen array dan nilai adalah jumlah kejadian. Kemudian pilih semua elemen yang muncul lebih dari satu kali. Mudah.

Sergio Tulentsev
sumber
7

Saya tahu utas ini tentang Ruby secara khusus, tetapi saya mendarat di sini mencari cara untuk melakukan ini dalam konteks Ruby on Rails dengan ActiveRecord dan berpikir saya akan membagikan solusi saya juga.

class ActiveRecordClass < ActiveRecord::Base
  #has two columns, a primary key (id) and an email_address (string)
end

ActiveRecordClass.group(:email_address).having("count(*) > 1").count.keys

Di atas mengembalikan array semua alamat email yang digandakan dalam tabel database contoh ini (yang dalam Rails akan menjadi "active_record_classes").

kode danielric
sumber
6
a = ["A", "B", "C", "B", "A"]
a.each_with_object(Hash.new(0)) {|i,hash| hash[i] += 1}.select{|_, count| count > 1}.keys

Ini adalah O(n)prosedur.

Atau Anda dapat melakukan salah satu dari baris berikut. Juga O (n) tetapi hanya satu iterasi

a.each_with_object(Hash.new(0).merge dup: []){|x,h| h[:dup] << x if (h[x] += 1) == 2}[:dup]

a.inject(Hash.new(0).merge dup: []){|h,x| h[:dup] << x if (h[x] += 1) == 2;h}[:dup]
benzhang
sumber
2

Inilah pendapat saya tentang kumpulan data - seperti tabel dBase lama untuk menemukan bagian duplikat

# Assuming ps is an array of 20000 part numbers & we want to find duplicates
# actually had to it recently.
# having a result hash with part number and number of times part is 
# duplicated is much more convenient in the real world application
# Takes about 6  seconds to run on my data set
# - not too bad for an export script handling 20000 parts

h = {};

# or for readability

h = {} # result hash
ps.select{ |e| 
  ct = ps.count(e) 
  h[e] = ct if ct > 1
}; nil # so that the huge result of select doesn't print in the console
konung
sumber
2
r = [1, 2, 3, 5, 1, 2, 3, 1, 2, 1]

r.group_by(&:itself).map { |k, v| v.size > 1 ? [k] + [v.size] : nil }.compact.sort_by(&:last).map(&:first)
Dorian
sumber
1

each_with_object adalah temanmu!

input = [:bla,:blubb,:bleh,:bla,:bleh,:bla,:blubb,:brrr]

# to get the counts of the elements in the array:
> input.each_with_object({}){|x,h| h[x] ||= 0; h[x] += 1}
=> {:bla=>3, :blubb=>2, :bleh=>2, :brrr=>1}

# to get only the counts of the non-unique elements in the array:
> input.each_with_object({}){|x,h| h[x] ||= 0; h[x] += 1}.reject{|k,v| v < 2}
=> {:bla=>3, :blubb=>2, :bleh=>2}
Tilo
sumber
1

Kode ini akan mengembalikan daftar nilai duplikat. Kunci hash digunakan sebagai cara yang efisien untuk memeriksa nilai mana yang sudah terlihat. Berdasarkan pada apakah nilai telah terlihat, array asli arydipartisi menjadi 2 array: pertama berisi nilai unik dan kedua berisi duplikat.

ary = ["hello", "world", "stack", "overflow", "hello", "again"]

hash={}
arr.partition { |v| hash.has_key?(v) ? false : hash[v]=0 }.last.uniq

=> ["hello"]

Anda dapat memperpendeknya - meskipun dengan biaya sintaksis yang sedikit lebih kompleks - ke formulir ini:

hash={}
arr.partition { |v| !hash.has_key?(v) && hash[v]=0 }.last.uniq
cryptogopher
sumber
0
a = ["A", "B", "C", "B", "A"]
b = a.select {|e| a.count(e) > 1}.uniq
c = a - b
d = b + c

Hasil

 d
=> ["A", "B", "C"]
Amrit Dhungana
sumber
0

Jika Anda membandingkan dua array yang berbeda (bukan satu terhadap dirinya sendiri) cara yang sangat cepat adalah dengan menggunakan operator interseksi yang &disediakan oleh kelas Array Ruby .

# Given
a = ['a', 'b', 'c', 'd']
b = ['e', 'f', 'c', 'd']

# Then this...
a & b # => ['c', 'd']
IAmNaN
sumber
1
Itu menemukan item yang ada di kedua array, bukan duplikat dalam satu array.
Kimmo Lehto
Terima kasih telah menunjukkannya. Saya telah mengubah kata-kata dalam jawaban saya. Saya akan meninggalkannya di sini karena sudah terbukti bermanfaat untuk beberapa orang yang datang dari pencarian.
IAmNaN
0

Saya perlu mencari tahu berapa banyak duplikat yang ada dan apa yang jadi saya menulis fungsi membangun dari apa yang diposting Naveed sebelumnya:

def print_duplicates(array)
  puts "Array count: #{array.count}"
  map = {}
  total_dups = 0
  array.each do |v|
    map[v] = (map[v] || 0 ) + 1
  end

  map.each do |k, v|
    if v != 1
      puts "#{k} appears #{v} times"
      total_dups += 1
    end
  end
  puts "Total items that are duplicated: #{total_dups}"
end
muneebahmad
sumber
-1
  1. Mari kita buat metode duplikasi yang mengambil array elemen sebagai input
  2. Dalam tubuh metode, mari kita buat 2 objek array baru yang satu terlihat dan yang lain duplikat
  3. Akhirnya mari kita beralih melalui setiap objek dalam array yang diberikan dan untuk setiap iterasi memungkinkan menemukan objek yang ada di array yang terlihat.
  4. jika objek ada di seen_array, maka itu dianggap sebagai objek duplikat dan mendorong objek itu ke duplication_array
  5. jika objek tidak ada dalam yang terlihat, maka itu dianggap sebagai objek yang unik dan mendorong objek itu ke seen_array

mari kita tunjukkan dalam Implementasi Kode

def duplication given_array
  seen_objects = []
  duplication_objects = []

  given_array.each do |element|
    duplication_objects << element if seen_objects.include?(element)
    seen_objects << element
  end

  duplication_objects
end

Sekarang panggil metode duplikasi dan hasil pengembalian keluaran -

dup_elements = duplication [1,2,3,4,4,5,6,6]
puts dup_elements.inspect
Yugesh Palvai
sumber
Jawaban khusus kode hanya disukai pada situs ini. Bisakah Anda mengedit jawaban Anda untuk memasukkan beberapa komentar atau penjelasan kode Anda? Penjelasan harus menjawab pertanyaan seperti: Apa fungsinya? Bagaimana cara melakukannya? Kemana perginya? Bagaimana cara mengatasi masalah OP? Lihat: Bagaimana cara anwser . Terima kasih!
Eduardo Baitello
-4

[1,2,3].uniq!.nil? => true [1,2,3,3].uniq!.nil? => false

Perhatikan hal di atas merusak

Maks
sumber
ini tidak mengembalikan nilai duplikat
andriy-baran