画面にこう出れば正解
どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。
基本
$ python3 unique.py 3 1 3 2 1
3
1
2
3件(2件を消した)並べ替えない(集合に入れると順番が変わる言語がある)
$ python3 unique.py banana apple cherry apple banana date
banana
apple
cherry
date
4件(2件を消した)大文字と小文字は別
$ python3 unique.py a A a B b
a
A
B
b
4件(1件を消した)数としては同じでも、文字としては別
$ python3 unique.py 1 01 1.0 1
1
01
1.0
3件(1件を消した)濁点の付け方が違う「が」
$ python3 unique.py が が が
が
が
2件(1件を消した)空の文字列
$ python3 unique.py "" x "" y
(空)
x
y
3件(1件を消した)重複が無い
$ python3 unique.py 東京 大阪 名古屋
東京
大阪
名古屋
3件(0件を消した)全部同じ
$ python3 unique.py 🍣 🍣 🍣
🍣
1件(2件を消した)引数が無い
$ python3 unique.py
0件(0件を消した)10言語のプログラム
どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。
unique.php8.4.26 で 9 / 9 件の出力が正解と一致
<?php
// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
$items = array_slice($argv, 1);
// array_unique は初めて出たものを残し、既定(SORT_STRING)では文字列として比べるので "1" と "01" は別になる。
// in_array($x, $out) を第3引数なしで使うと「==」で比べ、"1" と "01" と "1.0" が同じ数とみなされる
$out = array_unique($items);
foreach ($out as $s) {
echo ($s === '' ? '(空)' : $s) . "\n";
}
// 消した分の添字が飛ぶので、数は count で数える
echo count($out) . '件(' . (count($items) - count($out)) . "件を消した)\n";
動かし方(ファイルを保存したフォルダで)
$ php unique.php 3 1 3 2 1
unique.py3.14.7 で 9 / 9 件の出力が正解と一致
# 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
import sys
items = sys.argv[1:]
# dict は入れた順を覚えている(3.7 から言語の決まり)。set は順を覚えないので、list(set(...)) は並びが変わる
out = list(dict.fromkeys(items))
for s in out:
print("(空)" if s == "" else s)
print(f"{len(out)}件({len(items) - len(out)}件を消した)")
動かし方(ファイルを保存したフォルダで)
$ python3 unique.py 3 1 3 2 1
unique.go1.27.2 で 9 / 9 件の出力が正解と一致
// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
package main
import (
"fmt"
"os"
)
func main() {
items := os.Args[1:]
// map を range で回す順はわざと毎回ばらばらにしてある。見たかどうかだけ map で覚え、出す順はスライスに残す
seen := map[string]bool{}
out := []string{}
for _, s := range items {
if !seen[s] {
seen[s] = true
out = append(out, s)
}
}
for _, s := range out {
if s == "" {
s = "(空)"
}
fmt.Println(s)
}
fmt.Printf("%d件(%d件を消した)\n", len(out), len(items)-len(out))
}
動かし方(ファイルを保存したフォルダで)
$ go run unique.go 3 1 3 2 1
unique.js24.15.0 で 9 / 9 件の出力が正解と一致
// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
const items = process.argv.slice(2);
// Set は入れた順を覚えている。オブジェクトのキーで覚えると "1" "3" のような整数に見えるキーが小さい順に前へ並ぶ
const out = [...new Set(items)];
for (const s of out) {
console.log(s === "" ? "(空)" : s);
}
console.log(`${out.length}件(${items.length - out.length}件を消した)`);
動かし方(ファイルを保存したフォルダで)
$ node unique.js 3 1 3 2 1
Unique.java21.0.12 で 9 / 9 件の出力が正解と一致
// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
public class Unique {
public static void main(String[] args) {
// HashSet は並びがハッシュ値で決まる。入れた順を覚えるのは LinkedHashSet。
// String の equals は char を1つずつ比べるので、「が」1文字と「か」+濁点は別になる
Set<String> out = new LinkedHashSet<>(Arrays.asList(args));
for (String s : out) {
System.out.println(s.isEmpty() ? "(空)" : s);
}
System.out.println(out.size() + "件(" + (args.length - out.size()) + "件を消した)");
}
}
動かし方(ファイルを保存したフォルダで)
$ java Unique.java 3 1 3 2 1
unique.cs10.0.402 で 9 / 9 件の出力が正解と一致
// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
// Distinct は初めて出た順に返し、既定では文字の並び(ordinal)で比べる。
// string.Compare(a, b) == 0 で比べると、カルチャの規則で「が」1文字と「か」+濁点を同じとみなすことがある
var kept = args.Distinct().ToList();
foreach (string s in kept)
{
Console.WriteLine(s == "" ? "(空)" : s);
}
Console.WriteLine($"{kept.Count}件({args.Length - kept.Count}件を消した)");
動かし方(ファイルを保存したフォルダで)
$ dotnet run unique.cs -- 3 1 3 2 1
unique.rs1.99.0 で 9 / 9 件の出力が正解と一致
// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
use std::collections::HashSet;
fn main() {
let items: Vec<String> = std::env::args().skip(1).collect();
// dedup は隣り合う重複しか消さないので、sort してから使うと並びが変わる。
// HashSet の insert は初めて入れたときだけ true を返すので、retain で初めてのものだけ残す
let mut seen = HashSet::new();
let mut out = items.clone();
out.retain(|s| seen.insert(s.clone()));
for s in &out {
println!("{}", if s.is_empty() { "(空)" } else { s });
}
println!("{}件({}件を消した)", out.len(), items.len() - out.len());
}
動かし方(ファイルを保存したフォルダで)
$ rustc unique.rs && ./unique 3 1 3 2 1
unique.rb4.0.7 で 9 / 9 件の出力が正解と一致
# 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
items = ARGV
# uniq は初めて出た順を保つ。比べるのは文字の並び(バイト)なので、「が」1文字と「か」+濁点は別になる
out = items.uniq
out.each { |s| puts(s.empty? ? "(空)" : s) }
puts "#{out.size}件(#{items.size - out.size}件を消した)"
動かし方(ファイルを保存したフォルダで)
$ ruby unique.rb 3 1 3 2 1
unique.kt2.4.21 で 9 / 9 件の出力が正解と一致
// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
fun main(args: Array<String>) {
// distinct は初めて出た順を保つ(中は LinkedHashSet)。hashSetOf に入れて出すと並びがハッシュ値の順になる
val out = args.distinct()
for (s in out) {
println(if (s.isEmpty()) "(空)" else s)
}
println("${out.size}件(${args.size - out.size}件を消した)")
}
動かし方(ファイルを保存したフォルダで)
$ kotlinc unique.kt -include-runtime -d unique.jar && java -jar unique.jar 3 1 3 2 1
unique.swift6.3.3 で 9 / 9 件の出力が正解と一致
// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
let items = Array(CommandLine.arguments.dropFirst())
// Swift の String の == と Set<String> は、見た目が同じ(正規化して同じ)なら同じとみなす。
// 「が」1文字と「か」+濁点を別にするため、UTF-8 のバイトの並びで覚える
var seen = Set<[UInt8]>()
let out = items.filter { seen.insert(Array($0.utf8)).inserted }
for s in out {
print(s.isEmpty ? "(空)" : s)
}
print("\(out.count)件(\(items.count - out.count)件を消した)")
動かし方(ファイルを保存したフォルダで)
$ swift unique.swift 3 1 3 2 1
よく見かける書き方で試すと
仕事でよく見かける書き方(標準の関数をそのまま使う形や、よく紹介される正規表現など)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。
| 入力 | よく見かける書き方の答え(8言語) |
|---|---|
基本3 1 3 2 1 |
|
並べ替えない(集合に入れると順番が変わる言語がある)banana apple cherry apple banana date |
|
大文字と小文字は別a A a B b |
|
数としては同じでも、文字としては別1 01 1.0 1 |
|
濁点の付け方が違う「が」が が が |
|
空の文字列"" x "" y | ✓8言語すべて正解 |
重複が無い東京 大阪 名古屋 |
|
全部同じ🍣 🍣 🍣 | ✓8言語すべて正解 |
| 引数が無い | ✓8言語すべて正解 |
この書き方の決まり
- 同じ文字列の2回目以降を消し、初めて出た順に1行ずつ出す。最後に残った数と消した数を出す
- 大文字と小文字は別のもの。
1と01も別のもの - 文字は、見た目ではなく Unicode の文字の並びで比べる(「が」1文字と「か」+濁点の2文字は別)
- 空の文字列も1つの値として扱う
解説
順番を保つ道具を使う
多くの言語に、入れた順を覚えたまま重複を消す道具があります。
| 言語 | 書き方 |
|---|---|
| Python | list(dict.fromkeys(items))(dict は入れた順を保つ) |
| JavaScript | [...new Set(items)](Set は入れた順を保つ) |
| Java | new LinkedHashSet<>(…) |
| Kotlin | distinct() |
| C# | Distinct() |
| Ruby | uniq |
| PHP | array_unique(キーは元の位置のまま残り、添字が飛ぶ。0 から振り直すなら array_values) |
| Go・Rust・Swift | 見たかどうかを集合で覚え、出す順は元の並びで決める |
集合に入れると、順番が変わる
| よく見かける書き方 | 起きること |
|---|---|
Java new HashSet<>(…)・Kotlin toHashSet() | ハッシュの順になる。同じ入力でも、Java と Kotlin で並びが違った(最初に取る大きさが違うため) |
Python list(set(…))・Go の map を range で回す | 実行するたびに順番が変わる(ハッシュの種や回す順を、毎回わざと変えている) |
Python sorted(set(…))・Go のキーを sort・Rust の sort() と dedup() | 決まった順にはなるが、元の順ではなく文字の番号の順 |
| JavaScript のオブジェクトのキーで覚える | 整数に見えるキー("3" "1")だけ、小さい順に前へ並ぶ |
「同じ」の決め方
- PHP の
in_arrayは、何も指定しないと==で比べるので、"1"と"01"と"1.0"を同じとみなします。第3引数にtrueを渡すと、型も中身も同じかで比べます - Swift の String の
==は、正規化して比べます。「が」1文字と「か」+濁点(U+3099)が同じになり、Set<String>では1つに減ります。この書き方では、文字の並び(UTF-8 のバイト列)で比べています
どちらも、目的によっては正しい動きです。「見た目が同じなら同じ」にしたいなら、Swift のほうが便利です。この書き方の決まりは「文字の並びが同じなら同じ」です。