The most complete quick sorting method in the whole network--Hoare quick sorting method, quick sorting, two-way quick sorting, three-way quick sorting, non-recursive quick sorting

Table of contents

1. Quick sort

1. Basic introduction

2. Basic idea

2. Hoare quick row

0. Prior knowledge

1. Swap two elements in an array

2. Insertion sort of specified range

1. Basic ideas

2. Code implementation

3. Optimization ideas

3. Quick queue by digging holes (applicable to school recruitment)

1. Basic ideas

2. Code implementation

Four. Two-way quick queue

1. Basic ideas

2. Code implementation

3. Optimization ideas

Five. Three-way quick queue

1. Basic ideas

2. Code implementation

6. Implementation of non-recursive quick sort

1. Thinking analysis

2. Code implementation


1. Quick sort

1. Basic introduction

Quicksort (Quicksort) was invented by British computer scientist Tony Hoare in 1959. It is a classic sorting algorithm and is widely used in the field of computer science. Quick sort (Quick Sort) is a common sorting algorithm based on comparison , and it is also one of the most commonly used sorting algorithms.

Quick sort is a

sorting method
most
average
the worst
space complexity
stability
quick sort
O(n * log(n))
O(n * log(n)) O(n^2) O(log(n)) ~ O(n) unstable

Stability: If a is originally in front of b, and a=b, after sorting, a is still in front of b, then the algorithm is stable, otherwise it is unstable;

2. Basic idea

The following is the basic idea of ​​quick sort

  1. Choose a pivot.

  2. Places all elements in the sequence that are smaller than the pivot element to the left of the pivot element, and all elements that are greater than the pivot element to the right.

  3. Recursively sorts the subsequences to the left and right of the pivot element.

  4. After the recursion ends, the entire sequence becomes sorted.

The various fast sorting ideas introduced below are roughly the same, the main reason is that the division strategies are different, and the basic ideas are the same

2. Hoare quick row

0. Prior knowledge

1. Swap two elements in an array

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

2. Insertion sort of specified range

    /**
     * 将区间[left,right]的元素进行插入排序
     *
     * @param arr
     * @param left
     * @param right
     */
    private static void insertSortInterval(int[] arr, int left, int right) {
        for (int i = left + 1; i <= right; ++i) {
            for (int j = i; j > left && arr[j] < arr[j - 1]; --j) {
                swap(arr, j, j - 1);
            }
        }
    }

1. Basic ideas

Here we focus on the method of partitioning, because the difference between these quick sorts is mainly in the method of partitioning. We have already mentioned the basic idea of ​​quick sorting above, so I won’t go into details here.

Here to explain the method of partitioning, that is, the code of the partition part.

In fact, it is simple and easy to understand. We choose the value at the left end as the central axis value, and define two pointers i, j, j to traverse from the right end to the left. When traversing to a number smaller than the central axis value (pivot) , stop, i traverses from the left end to the right, when it traverses to a value larger than the central axis, stop, at this time exchange the elements at position i and position j, when i==j, stop the loop, and then exchange the position left And the elements of i(j), at this time, the element on the left of i is smaller than arr[i], and the element on the right of i is larger than arr[i], return the position i at this time.

Initial situation:

first exchange

second exchange

 end loop

Middle axis value exchange operation

At this time, the elements on the left of pivot=6 are smaller than the value of 6, and the values ​​on the right are larger than the pivot, so that the partition ends.

2. Code implementation

    //Hoare版快排
    public static void quickSortHoare(int[] arr) {
        quickSortHoareInternal(arr, 0, arr.length - 1);
    }

    private static void quickSortHoareInternal(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }
        int pivotIndex = partitionHoare(arr, left, right);
        quickSortHoareInternal(arr, left, pivotIndex - 1);
        quickSortHoareInternal(arr, pivotIndex + 1, right);

    }

    private static int partitionHoare(int[] arr, int left, int right) {
        //选取最左边的元素作为中轴值
        int pivot = arr[left];
        int i = left, j = right;
        while (i < j) {
            //从右边找到比pivot小的元素
            while (i < j && arr[j] >= pivot) {
                j--;
            }

            //从左边找到比pivot大的元素
            while (i < j && arr[i] <= pivot) {
                i++;
            }
            swap(arr, i, j);

        }
        //将中轴值元素和i==j时的位置交换,此时i左边的元素都比pivot小,右边都比pivor大
        swap(arr,i,left);
        return i;

    }

3. Optimization ideas

Optimization 1 : After studying insertion sorting, we know that for small arrays, the efficiency of insertion sorting is very high. For small arrays with a length less than 64, we might as well use insertion sorting directly for sorting

    private static void quickSortHoareInternal(int[] arr, int left, int right) {
        if (right - left <= 64) {
            insertSortInterval(arr, left, right);
            return;
        }
        int pivotIndex = partitionHoare(arr, left, right);
        quickSortHoareInternal(arr, left, pivotIndex - 1);
        quickSortHoareInternal(arr, pivotIndex + 1, right);

    }

Optimization 2 : Let's consider such a problem. When the array we need to sort is basically ordered, we still choose the first element of the array as the axis value every time, so we have to recurse O(n) space complexity At this time, the quick sort degenerates into a bubble sort, and the time complexity is O( n^{2})

So how do we choose the median value?

The first method: randomly select the central axis value

Generate a random number with a range of [left, right] to generate a subscript as index, exchange index with left, and then it will be exactly the same as our previous code.

    private static int partitionHoare(int[] arr, int left, int right) {
        //优化,选取随机值
        int index = ThreadLocalRandom.current().nextInt(left, right + 1);
        swap(arr, index, left);
        int pivot = arr[left];
        int i = left, j = right;
        while (i < j) {
            //从右边找到比pivot小的元素
            while (i < j && arr[j] >= pivot) {
                j--;
            }

            //从左边找到比pivot大的元素
            while (i < j && arr[i] <= pivot) {
                i++;
            }
            swap(arr, i, j);

        }
        swap(arr, i, left);
        return j;

    }

The second method: three-number median division method

We all hope that the median axis value we choose is exactly the median value of the array to be sorted, so that the number of recursions must be the least, so we can use the method of taking the middle of three numbers to estimate the median value (of course, the worst case may also be to the second smallest case, but the probability is relatively small), we usually choose the median of the three elements on the left end, right end, and center as the pivot element. Let’s see the code implementation

    public static void median(int[] arr, int left, int right) {
        //中间索引下标,相当于(left+right)/2
        int center = left + ((right - left) >> 1);
        if (arr[center] < arr[left]) {
            swap(arr, center, left);
        }
        if (arr[right] < arr[left]) {
            swap(arr, right, left);
        }
        if (arr[right] < arr[center]) {
            swap(arr, center, right);
        }
        swap(arr, left, center);//此时中值被交换到了最左边位置
    }

    private static int partitionHoare(int[] arr, int left, int right) {
        median(arr,left,right);
        int pivot = arr[left];
        int i = left, j = right;
        while (i < j) {
            //从右边找到比pivot小的元素
            while (i < j && arr[j] >= pivot) {
                j--;
            }

            //从左边找到比pivot大的元素
            while (i < j && arr[i] <= pivot) {
                i++;
            }
            swap(arr, i, j);

        }
        swap(arr, i, left);
        return j;

    }

3. Quick queue by digging holes (applicable to school recruitment)

1. Basic ideas

The basic idea of ​​the pit digging method for fast sorting: we first keep the value of the pivot, which is equivalent to digging a pit at the pivot position (that is, the left position), and then we start looping. The looping conditions are the same as Hoare quick sorting, j Find an element smaller than pivot from the right end to the left, fill the value of arr[j] into the pit dug before (that is, the left position), and then dig a pit at j position, i starts from the left end to find an element larger than pivot, After finding it, fill it in position j (the pit dug before), and so on, finally i==j, this is the last pit dug, fill the pivot value in the position of arr[i], at this time we dig The hole filling operation is completed, the elements on the left of arr[i] are smaller than it, and the elements on the right are larger than it

It can be seen that the number of exchanges is greatly reduced compared to Hoare's quick queue

 first time digging

 Second digging and filling

 The third digging and filling

 The fourth digging and filling

 The fifth digging and filling

Last time arr[i]=pivot

2. Code implementation

The optimized place is the same as Hoare's quick sorting, here is the optimized code directly, and the method of selecting the central axis value by random value is used here.

    //挖坑法快排
    public static void quickSortHole(int[] arr) {
        quickSortHoleInternal(arr, 0, arr.length - 1);
    }

    private static void quickSortHoleInternal(int[] arr, int left, int right) {
        if (right - left <= 64) {
            insertSortInterval(arr, left, right);
            return;
        }
        int pivotIndex = partitionByHole(arr, left, right);
        quickSortHoleInternal(arr, left, pivotIndex - 1);
        quickSortHoleInternal(arr, pivotIndex + 1, right);

    }

    private static int partitionByHole(int[] arr, int left, int right) {
        //优化,选取随机值
        int index = ThreadLocalRandom.current().nextInt(left, right + 1);
        swap(arr, index, left);
        int pivot = arr[left];
        int i = left, j = right;
        while (i < j) {
            //从右边找到比pivot小的元素
            while (i < j && arr[j] >= pivot) {
                j--;
            }
            //将这个小的元素放到左边
            arr[i] = arr[j];
            //从左边找到比pivot大的元素
            while (i < j && arr[i] <= pivot) {
                i++;
            }
            //将这个大的元素放到右边
            arr[j] = arr[i];

        }
        //最后一定是i==j退出
        arr[j] = pivot;
        return j;

    }

Four. Two-way quick queue

1. Basic ideas

The second-way quick sorting is actually carried out in partitions, and it is also the quick sorting method (algorithm 4) implemented in foreign textbooks. The overall efficiency is faster than the digging method and the Hoare method.

The main thing is to maintain two partitions. The value of one partition is smaller than the value of pivot, and the value of the other partition is greater than or equal to the value of pivot. The final result after partitioning is shown in the following figure:

The maintained interval smaller than pivot is [left+1,j] The maintained interval greater than or equal to pivot is [j+1,i-1] where i points to the element position being traversed, and j points to the interval smaller than pivot The last position of j+1 is the starting position greater than or equal to the pivot. The white area represents the area that has not been traversed.

Now let's discuss two situations, one is when arr[i]>=pivot , at this time we only need to turn the position pointed by i into yellow, that is, process i++, this is the interval greater than or equal to pivot It is equivalent to adding an element

The other is when arr[i]<pivot , at this time we only need to exchange the elements of arr[i] and arr[j+1], and j++, and then i++, so that it is less than pivot One element is added to the range.

This kind of loop continues until i has traversed the last element. At this time, we exchange the element at the j position with the element at the left position, so that the element on the left of the pivot element is smaller than the pivot, and the element on the right of the pivot is greater than or equal to pivot.

 The specific code implementation is as follows

2. Code implementation

    // 算法4的分区快排
    public static void quickSort(int[] arr) {
        quickSortInternal(arr, 0, arr.length - 1);
    }

    private static void quickSortInternal(int[] arr, int left, int right) {
        if (right - left <= 64) {
            insertSortInterval(arr, left, right);
            return;
        }
        int p = partition(arr, left, right);
        quickSortInternal(arr, left, p - 1);
        quickSortInternal(arr, p + 1, right);
    }

    //二路快排,左半部分小于pivot,右半部分大于等于pivot
    private static int partition(int[] arr, int left, int right) {
        int randomIndex = ThreadLocalRandom.current().nextInt(left, right + 1);
        swap(arr, left, randomIndex);
        int pivot = arr[left];
        // arr[l + 1..j] < pivot
        int j = left;
        // arr[j + 1..i) >= pivot
        for (int i = left + 1; i <= right; i++) {
            if (arr[i] < pivot) {
                swap(arr, j + 1, i);
                j++;
            }
        }
        swap(arr, j, left);
        return j;
    }

3. Optimization ideas

When we have a large number of identical elements, the efficiency of the two-way quick sort is actually not very fast at this time, because many times of useless recursive processing is required. At this time, can we consider dividing into a separate partition that is equal to the pivot? Indeed It is achievable. At this time, we adopt the method of three-way quick sorting, which can solve the problem we mentioned to a large extent.

Five. Three-way quick queue

1. Basic ideas

The implementation idea of ​​the three-way quick sort is basically the same as that of the two-way quick sort, except that one more interval equal to the pivot is maintained, so that when we recurse, there is no need to recursively carry out the interval equal to the pivot, which greatly improves The efficiency of quick sort.

The following is the layout of the three-way quick queue partition:

Maintain the interval smaller than pivot as [left, lt], maintain the interval equal to pivot as [lt+1, i-1], and maintain the interval greater than pivot as [gt, right]. i represents the position index being traversed, when When i>=gt, the white area at the end of the traversal indicates the area that has not been traversed.

Three-way quick queue has three situations to consider

1. When arr[i]>pivot, we can exchange arr[i] with arr[lt+1], and increase the interval of i++, which is smaller than pivot at this time

2. When arr[i]==pivot, we can directly add i++. At this time, the interval equal to pivot increases, and the processing ends

3. When arr[i]>pivot , we can exchange arr[i] with arr[gt-1], and gt--, means that the interval greater than pivot increases. At this time, i does not need to increase, because the exchange The area is a white area, which has not been traversed, we need to judge next time

 Finally, when i==gt, the loop ends, and we exchange the elements of arr[left] and arr[lt]

The recursive condition is now left-->lt-1 gt-->right

2. Code implementation

    //三路快排(对于处理含有重复元素的数组很有效)
    public static void quickSort3(int[] arr) {
        quickSort3Internal(arr, 0, arr.length - 1);
    }

    private static void quickSort3Internal(int[] arr, int left, int right) {
        if (right - left <= 64) {
            insertSortInterval(arr, left, right);
            return;
        }
        int radomIndex = ThreadLocalRandom.current().nextInt(left, right + 1);
        //此时left位置为中轴值
        swap(arr, left, radomIndex);
        int pivot = arr[left];
        //区间[left+1,lt]的值<pivot
        int lt = left;
        //区间[gt,right]的值>pivot
        int gt = right + 1;
        //区间(lt,gt)的元素==pivot
        //目前遍历到的位置
        int i = left + 1;
        //终止条件i与gt重合
        while (i < gt) {
            if (arr[i] < pivot) {
                swap(arr, i, lt + 1);
                lt++;
                i++;
            } else if (arr[i] > pivot) {
                swap(arr, i, gt - 1);
                gt--;
            } else {//相等的情况
                i++;

            }
        }
        swap(arr, left, lt);
        quickSort3Internal(arr, left, lt - 1);
        quickSort3Internal(arr, gt, right);

    }

6. Implementation of non-recursive quick sort

1. Thinking analysis

In fact, non-recursive quick sorting is also very easy to implement. If we carefully observe the previous code, we can find that, in fact, the quick sorting code is very similar to the preorder traversal of a binary tree. It is processed first, then recursively processed to the left, and then to the right Recursive processing: If you are not clear about the implementation of binary tree recursion and iteration, you can read this blog: tree traversal methods (front, middle, back, layer order traversal, recursion, iteration, Morris traversal)

We used the stack structure when implementing the pre-order traversal of the iterative binary tree. Here we also need to borrow the stack to implement the non-recursive implementation of quick sort. Then what information do we want to save in the stack? We are performing recursion to achieve fast When sorting, each recursion is the recursion that left and right are changed. Here we must save left and right, and then take out left and right for partition processing, but we need to pay attention to the saved The order should be reversed, that is, save right first, then save left, because the nature of the stack: first in last out, so as to ensure that it is consistent with the recursive order.

See the implementation of the code for details.

2. Code implementation

    //非递归快排(挖坑)
    public static void quickSortNoRecursion(int[] arr) {
        LinkedList<Integer> stack = new LinkedList<>();
        stack.push(arr.length - 1);
        stack.push(0);
        while (!stack.isEmpty()) {
            Integer left = stack.pop();
            Integer right = stack.pop();
            if (right - left <= 64) {
                insertSortInterval(arr, left, right);
                continue;
            }
            int pivotIndex = partitionByHole(arr, left, right);
            stack.push(right);
            stack.push(pivotIndex + 1);
            stack.push(pivotIndex - 1);
            stack.push(left);

        }


    }
    //这里使用的挖坑法快排的划分方式,也可以使用别的快排的划分方式
    private static int partitionByHole(int[] arr, int left, int right) {
        //优化,选取随机值
        int index = ThreadLocalRandom.current().nextInt(left, right + 1);
        swap(arr, index, left);
        int pivot = arr[left];
        int i = left, j = right;
        while (i < j) {
            //从右边找到比pivot小的元素
            while (i < j && arr[j] >= pivot) {
                j--;
            }
            //将这个小的元素放到左边
            arr[i] = arr[j];
            //从左边找到比pivot大的元素
            while (i < j && arr[i] <= pivot) {
                i++;
            }
            //将这个大的元素放到右边
            arr[j] = arr[i];

        }
        //最后一定是i==j退出
        arr[j] = pivot;
        return j;

    }

Guess you like

Origin blog.csdn.net/qq_64580912/article/details/130211717