Fundamentals of algorithm – sorting out key points of quick sorting

Keywords: Java Algorithm

Recently, I reviewed the sorting algorithm. Although I have studied it before, I forget it almost if I write it directly, even if I have written relevant before article , let's summarize the idea of quick sorting here.

First, post the animation demonstration of quick row here:

partition

If you want to remember the implementation of fast scheduling, you only need to remember the core code of fast scheduling, that is, the idea of partitioning: given an array, take the last element as the benchmark, and after the partition operation, the data less than or equal to the benchmark element is on the left side of the array, and the data greater than the benchmark element is on the right side of the array. The returned result is the last index equal to the base element.

Remember the principle of partition algorithm, you understand the essence of fast scheduling, and realize the overall array order by gradually partitioning according to the benchmark elements!

Specific implementation of partition method (note that it may not be the entire array, but also some elements inside the array. In order to reuse, the head and tail positions of elements to be compared are passed here):

public static int partition(int[] arr, int L, int R) {
    if (L > R) {
        return -1;
    }

    if (L == R) {
        return L;
    }

	// Slow pointer: the record is less than or equal to the rightmost position of the area, and the initial value is excluding any elements
    int lessEqual = L - 1;
	// Fast pointer: iterates over all optional elements
    int index = L;
    while (index < R) {
        // Note that the area less than or equal to is placed on the left, otherwise there is still a case equal to the reference element on the right
        if (arr[index] <= arr[R]) {
            swap(arr, ++lessEqual, index);
        }
        index++;
    }
    swap(arr, R, ++lessEqual);
    return lessEqual;
}

In the above implementation, the returned result indicates the range less than or equal to the benchmark element. To quickly schedule to the implementation, we need to continue calling the method recursively, as follows:

public static void quickSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }

    process1(arr, 0, arr.length - 1);
}

public static void process1(int[] arr, int L, int R) {
    if (L >= R) {
        return;
    }
    int M = partition(arr, L, R);
    process1(arr, L, M - 1);
    process1(arr, M + 1, R);
}

However, there is a defect in the implementation of the above partition method. For example, we encounter such an array: [1, 2, 2, 2, 3, 2, 2]. Although we can sort according to the above method, we have done a lot of useless work, because after calling the partition method for the first time to partition according to the value 2, the array has been orderly, However, the program will continue to partition according to 2 on the left, and repeated comparisons are made. How to optimize it?

Partition (enhanced)

The implementation of the above partition method only returns a numerical value to represent the partition result. The partition actually needs to be divided into three areas: less than area, equal to area and greater than area on the left. Therefore, we can return two values from the partition method to identify three regions: that is, equal to the left and right positions of the region.

The specific implementation code is as follows:

public static int[] partition2(int[] arr, int L, int R) {
    if (L > R) {
        return new int[]{-1, -1};
    }

    if (L == R) {
        return new int[]{L, R};
    }

    // Less than the area pointer, which does not include any elements by default
    int less = L - 1;
    // Greater than the area pointer, which does not include any elements by default
    int more = R;

    // Fast pointer: traverses all elements
    int index = L;
    while (index < more) {
        // Based on the rightmost element arr[R]
        if (arr[index] == arr[R]) {
            // When equal, the fast pointer is incremented directly
            index++;
        } else if (arr[index] < arr[R]) {
            // Expand less than area
            swap(arr, ++less, index++);
        } else if (arr[index] > arr[R]) {
            // Note that after changing the elements here, the index position elements have not been compared, so no self increment is performed
            swap(arr, --more, index);
        }
    }
    // Exchange the rightmost position elements and replace them to the appropriate position. At this time, the more pointer points to the rightmost position of the area
    swap(arr, R, more);
    // Returns the interval position equal to the area range
    return new int[]{less + 1, more};
}

Corresponding peripheral Code:

public static void quickSort2(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }

    process2(arr, 0, arr.length - 1);
}

public static void process2(int[] arr, int L, int R) {
    if (L >= R) {
        return;
    }
    int[] M2 = partition2(arr, L, R);

    process2(arr, L, M2[0] - 1);
    process2(arr, M2[1] + 1, R);
}

Select base element

In the above implementation, we select the rightmost element as the reference element, but in fact, if we first select 7 as the reference element for the originally ordered parameters: [1, 2, 3, 4, 5, 7], the above implementation time complexity will be as poor as O(n^2). Therefore, selecting the fixed position element as the reference data is defective, and the following optimization is carried out:

int i = L + (int) (Math.random() * (R - L + 1));
swap(arr, i, R);

// call partition

Summary

The above is the gradual evolution of fast scheduling implementation. The core method is the use of partition method, and then the overall array order is realized through gradual recursion!

Posted by shergold on Sun, 05 Sep 2021 16:28:51 -0700