Javaで0、1、2の配列をソートする

Write a program to sort an array of 0's,1's and 2's in ascending order.

Input:

The first line contains an integer 'T' denoting the total number of test cases. In each test cases, First line is number of elements in array 'N' and second its values.

Output:

Print the sorted array of 0's, 1's and 2's.

Constraints:

  • 1 <= T <= 100
  • 1 <= N <= 105
  • 0 <= arr[i] <= 2

Example:

Input :

2
5
0 2 1 2 0
3
0 1 0

Output:

0 0 1 2 2
0 0 1

私のアプローチ:

import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;

class GFG {

    private static void printSortedArr (int[] arr) {
        int count0 = 0, count1 = 0, count2 = 0;

        for (Integer elem : arr) {
            if (elem == 0) {
                count0++;
            }
            else if (elem == 1) {
                count1++;
            }
            else {
                count2++;
            }
        }

        while (count0 != 0) {
            System.out.print(0 + " ");
            count0--;
        }

        while (count1 != 0) {
            System.out.print(1 + " ");
            count1--;
        }

        while (count2 != 0) {
            System.out.print(2 + " ");
            count2--;
        }
        System.out.println();
    }

    public static void main (String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line = br.readLine();
        int numTests = Integer.parseInt(line);

        for (int i = 0; i < numTests; i++) {
            String line2 = br.readLine();
            int size = Integer.parseInt(line2);
            int[] arr = new int[size];
            String line3 = br.readLine();
            String[] inps = line3.split(" ");

            for (int j = 0; j < size; j++) {
                arr[j] = Integer.parseInt(inps[j]);
            }

            printSortedArr(arr);
        }
    }
}

上記のコードに関して以下の質問があります。

  1. 私のアプローチをさらに改善するにはどうすればよいですか?

  2. この問題を解決するもっと良い方法はありますか?

  3. 私が犯した重大なコード違反はありますか?

  4. スペースと時間の複雑さをさらに改善できますか?

参照

5

5 答え

わかりやすい名前

  private static void printSortedArr(int [] arr){
 

私はこれらの名前が好きではありません。それが言われているのなら私はむしろ配列を書きたいと思います。例えば。

    private static void printSortedArray(int[] numbers) {

今、私は Arr が配列の略語であることを理解する必要はありません。とにかく numbers は配列よりももっと説明的です。

番号付き変数よりも配列を優先する

  int count0 = 0、count1 = 0、count2 = 0。
 

そのため、ここでは名前に基づいて異なる値を保持する3つの変数があります。しかし、その関係を強制するデータ型、配列があります。

こちらの提案どおり

    private static void printSortedArray(int[] array, int maxElement) {
        int[] counts = new int[maxElement + 1];

今、あなたはより再利用可能な方法を持っています。 0、1、2の配列を並べ替えるだけではなくなりました。 0、...、 maxElement の任意の配列を並べ替えることができます。さらに、それは今自己組織化しています。の代わりに

  if(elem == 0){
                count0 ++;
            }
            そうでなければ(elem == 1){
                count1 ++;
            }
            その他{
                count2 ++;
            }
 

あなたはただ言うことができます

            counts[element]++;

新しい要素値を追加するたびに if / else を展開する必要がなくなりました。

コメントで、あなたが言った

配列に値を格納するのはスペースオーバーヘッドではありませんか?

元の3つの変数とこの配列の違いは length 変数です。それ以外は、両方とも同じスペースを使用します。追加の変数のコストは、 if / else モンスターを単純な配列の更新で置き換えることによるエラーの減少を補う以上のものです。

たとえば、配列に0、1、または2以外の数が含まれている場合、オリジナルにはバグがあります。それ以外の数は暗黙のうちに2として数えられます。修正版では、配列の範囲外の例外がスローされます。あなたの側で追加のチェックなしでそうしてください。

別のコメントあなたは言う

問題の説明によると、ソートされた要素を配列に明示的に格納する必要はないと考えていたので、それらを印刷するだけで済みました。また、配列を使用することはスペースのオーバーヘッドになると思いました。

その場合、配列は元の配列を複製し、スペースオーバーヘッドでした。しかし、そこで得られるのは、作成した配列を他のものに再利用できるということです。

あなたは、現在の問題の記述が別の行動と表示を必要としないことを指摘するのは正しいです。私たちがあなたに伝えようとしているのは、現実の世界では、問題の記述が変わるということです。あなたのコードは壊れやすいです。問題の記述が変わった場合は、それを書き直す必要があります。目標が変わっても機能し続ける堅牢なコードを書くことをお勧めします。これは、プロジェクトの目標が大きく変わる傾向があるためです。

Donald Knuth said:

本当の問題は、プログラマが間違った場所や間違った時に効率について心配するのにあまりにも多くの時間を費やしたことです。時期尚早の最適化は、プログラミングにおけるすべての悪(または少なくともその大部分)の根本です。

あなたは一般的に良いエンジニアリング原則のために書くことをお勧めしますそしてそれから、それが重要であるそれらのまれな例では、最適化します。ユーザーの要求がそれらをくまなく踏み込むので、あなたの最適化された解決策に恋をしないでください。

各要素の数を印刷するように求められた場合はどうなりますか?そのためには、このコードを修正する必要があります。しかし、あなたの方法が

   //numbers can only contain values from 0 to maximumValue
    private static int[] countElementsByValue(int[] numbers, int maximumValue) {
        int[] counts = new int[maximumValue + 1];

        for (int element : numbers) {
            counts[element]++;
        }

        return counts;
    }

それからあなたは簡単にカウントや配列を表示することができます。または両方。その情報がすぐそこにあるからです。元の方法では、このように書き直すか、その表示を行うために要素を再カウントする必要があります。元の方法はその情報をすぐに破棄するためです。または元の方法を変更することもできます。しかし、もし誰かが数え切れない配列だけが欲しいと言うとどうなりますか?

一般的に、複数のことを行うより複雑な方法よりも、1つのことを行う小さな方法、1つだけを行う方法が優先されます。これは、生成と表示が混在する場合に特に当てはまります。同じ基本データの上に複数の表示を要求することが非常に一般的であることがわかったので、これはかなり頻繁に失敗します。そのため、異なるアクションを別々にしておくための、より一般的なルールの特定の例(生成と表示を混在させないでください)があります。

あなたのオリジナルの方法は次のようになるかもしれません

        int[] counts = countElementsByValue(numbers, 2);
        int[] sorted = expandCounts(counts);
        System.out.println(Arrays.toString(sorted));

最後の行がソートされた配列を表示していることを知る必要がないことに注意してください。構いません。配列が何であれ、表示されます。

カウントから直接機能する特別な表示ルーチンを書くことで、おそらくパフォーマンスを向上させることができます。しかしそれほど多くの改善ではないでしょう。そして、ほとんどの場合、それは関係ありません。入力例では、時差はごくわずかです。プログラムを実行するよりもコンパイルに時間がかかります。

パフォーマンス

If you're really worried about storage パフォーマンス, then why read the data into an array at all?

            String line3 = br.readLine();
            String[] inps = line3.split(" ");

            for (int j = 0; j < size; j++) {
                arr[j] = Integer.parseInt(inps[j]);
            }

            printSortedArr(arr);

になり得る

            for (String token : br.readline().split(" ")) {
                 char digit = token.charAt(0);
                 if (digit == '0') {
                     System.out.print("0 ");
                 } else {
                     counts[digit - '0']++;
                 }
            }

            int value = '0';
            for (int count : counts) {
                while (count > 0) {
                    System.out.print(value + " ");
                    count--;
                }

                value++;
            }
            System.out.println();

One reason not to do this is that, counter-intuitively, it is actually slower to mix input and output like this than to use more storage. In particular, System.out.print is not high パフォーマンス with printing two characters at a time. You'd usually be better off using a StringBuilder to generate the string and then outputting that.

なぜ readline を使うのですか?結果として得られる配列は巨大になる可能性があり、あなたは2つの配列、1つは文字列、もう1つは整数の配列に自分自身をコミットしています。考えて

            int read = br.read();
            while (read >= 0) {
                if (read != ' ') {
                    counts[read - '0']++;
                }

                read = br.read();
            }

これにより、2つの配列と1つの文字列が節約され、1つの小さな配列が犠牲になります。本当にスペースが限られている場合は、このように入力と生成を混在させる方が、生成と出力を混在させるよりも優れています。

もちろん、これがあなたのオリジナルより遅いということはかなり可能です。これは一般的なトレードオフであり、スペースのスピードです。

Java 8では、(スタックオーバーフローから)のようなものを使用できます。

            for (i = 0; i < counts.length; i++) {
                String value = Integer.toString(i);
                System.out.print(String.join(" ", Collections.nCopies(counts[i], value)));
            }
            System.out.println();

これは4つのprintステートメントだけで表示されます。

あるいは、それがあまりにも多くのスペースを使用する場合(一時的なコレクションを作成するかどうかは私にはわかりません)、元の出力方法を使用してください。それは遅いかもしれませんが、それは非常に小さなスペースを使用します。

ただし、この種の最適化は、必要であることを確認した後にすべきことです。ほとんどの場合、これは不要になり、生産性が低下することさえあります。例えば。あなたが実際に時間的に制約されているときにあなたがスペースの制約について心配しているならば。

10
追加された
あなたの優れた提案をたくさんありがとう。これは私がこれまでに受け取った中で最も重要なアドバイスです。誰もこれを学校や資格で教えることはなく、あなたはあなたの答えを書くために多くの努力を払ってきました。再度、感謝します!!
追加された 著者 Dmitri Farkov,
@ RoToRa、チェックしてみましょう。
追加された 著者 Dmitri Farkov,
興味がある場合には: Collections.nCopies はカスタムの不変の List 実装を返しますが、実際にはArrayを埋めません(明示的に toArrayを呼び出さない限り)。 )内部的には含まれている要素とサイズを格納するだけで、他のすべてを「シミュレート」します。
追加された 著者 RoToRa,

あなたが実装したカウントソートは間違いなく進むべき道です。ただし、コードを少し整理します。

Boxing elem as an Integer, then unboxing it back to an int, is unjustified.

else if チェーンではなく switch ステートメントを書くと、ややきれいなバイトコードが生成されます。

You've mingled the output logic with the sorting logic, which is bad practice. You should define a separate function to do the output, especially since the main() is responsible for reading the input. The sorting function should be named to suggest that it only handles 0, 1, 2 as input.

出力の各行には、最後の数字の後に末尾のスペースが付きます。それはあなたの解決策がいくつかのテストに失敗する原因になるかもしれません。

Integer.parseInt()を呼び出すことが多いので、 Scanner を使用することをお勧めします。

import java.util.Arrays;
import java.util.Scanner;
import java.util.stream.Collectors;

class GFG {
    private GFG() {}

    public static void sort012(int[] array) {
        int count0 = 0, count1 = 0, count2 = 0;
        for (int elem : array) {
            switch (elem) {
                case 0: count0++; break;
                case 1: count1++; break;
                case 2: count2++; break;
                default: throw new IllegalArgumentException("Not 0, 1, or 2");
            }
        }
        int i = 0;
        while (count0-- > 0) { array[i++] = 0; }
        while (count1-- > 0) { array[i++] = 1; }
        while (count2-- > 0) { array[i++] = 2; }
    }

    public static String toString(int[] array) {
        return Arrays.stream(array)
                     .mapToObj(String::valueOf)
                     .collect(Collectors.joining(" "));
    }

    public static void main (String[] args) {
        Scanner scan = new Scanner(System.in);
        int numTests = scan.nextInt(); scan.nextLine();
        while (numTests-- > 0) {
            int size = scan.nextInt(); scan.nextLine();
            String[] inputs = scan.nextLine().split(" ");

            int[] array = new int[size];
            for (int j = 0; j < size; j++) {
                array[j] = Integer.parseInt(inputs[j]);
            }

            sort012(array);
            System.out.println(toString(array));
        }
    }
}
7
追加された
あなたの提案をありがとう。問題文によれば、ソートされた要素を配列に明示的に格納する必要はないと考え、それらを表示するだけにしました。また、配列を使用することはスペースのオーバーヘッドになると思いました。
追加された 著者 Dmitri Farkov,

それが正しいアプローチです。したがって、時間やスペースの複雑さを大幅に改善できるとは思いません。

ただし、個々の変数ではなく配列を使用してカウントを格納することで、関数の再利用性を向上させることができます。

private static void printSortedArray (int[] array, int maxElement) {
    int[] counts = new int[maxElement + 1];

    for (Integer element : array) {
       //You could check if 0 <= element < maxElement here
        counts[element]++;            
    }

    for(int i = 0; i < counts.length; i++) {
        for(; counts[i] > 0; counts[i]--) {
            System.out.print(i + " ");
        }
    }

    System.out.println();
}

また、上記で変更したように、変数名と関数名をわずかに省略することには意味がありません。追加の明快さは2文字長くなる価値があります。

さらに、このような関数は、ソートと印刷をくっつけるのではなく、より一般的には、配列を所定の位置に再配置するか、別のソートされた配列を返します。このプログラムのためにそれはそれが必要とすることをするが、一般的に言えばそれはあなたの論理をあなたの出力から切り離すのが良い。

5
追加された
@ Errratatz、提案をありがとう。配列に値を格納することはスペースオーバーヘッドではありませんか?
追加された 著者 Dmitri Farkov,
ボックス化/ボックス化解除を避けるには、(Integer element:array)の Integer ではなく int を使用します。
追加された 著者 fcarriedo,

私は簡潔でシンプルなコードのファンです。 Streamsがすでに印刷に使用されている場合は、なぜそれらも一緒にソートしないでください。このようにして、 String から int への変換およびその逆の変換は不要です。ただし、入力に1桁しかない場合にのみ機能します。もう1つの利点は、実際のデータの前に入力値として入力要素の数 'N'を個別に必要としないことです。そのため、 while ループの先頭に scan.nextLine(); を挿入して無視します。

これがあなたが望むことをしている完全なコードになるでしょう。

import static java.util.stream.Collectors.joining;
import java.util.Arrays;
import java.util.Scanner;

class GFG {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int numTests = scan.nextInt(); 
        scan.nextLine(); 
        while (numTests-- > 0) {
            scan.nextLine();
            String[] inputs = scan.nextLine().split(" ");
            System.out.println(Arrays.stream(inputs).sorted().collect(joining(" ")));
        }
        scan.close();
    }
}

別の発言:他の提案された解決策で使用されている BufferedReaderScanner のようなオブジェクトは常に閉じるべきです。

2
追加された
class GFG {
    private static void printSortedArr (int[] arr) {

"There are 2 hard problems in Computer Science: cache invalidation, naming, and off-by-1 errors." Poorly-chosen names can mislead the reader and cause bugs. It is important to use descriptive names that match the semantics and role of the underlying entities, within reason. What is GFG? Should arr already be sorted? If I just want to 3-way sort, can I do it without printing?

機能をシンプルにしてください。関数は単一の論理演算を実行する必要があります。それはあなたの実体が理解し、テストし、そして再利用することをより簡単にすることを可能にします。


            else {
                count2++;
            }
       //...
        while (count2 != 0) {
            System.out.print(2 + " ");
            count2--;
        }

これは本当ですか? 0でも1でもない要素は、デフォルトで2になりますか。


この問題を解決するもっと良い方法はありますか?時間と空間の複雑さをさらに改善できますか?

あなたのユースケースに依存します(測定!)。 3-wayカウンティングソートの他に、ピボットを取り、配列をピボットよりも小さい、等しい、または大きいグループに分割する3方向パーティション(クイックソート)も可能です。

...
  public static void threeWayPartition(int[] array, int pivot) {
    int first = 0;
    int mid = 0;
    int last = array.length - 1;

    while (mid <= last) {
      if (array[mid] < pivot) {
        swap(array, first++, mid++);
      }
      else if (array[mid] > pivot) {
        swap(array, mid, end--);
      }
      else {
        ++mid;
      }
    }
  }

  public static void swap(int[] array, int i, int j) {
    int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
  }
2
追加された