数据结构与算法之比较排序算法总结

原创
2020/03/13 14:40
阅读数 123

介绍:

比较算法大致可以分为两种,一种为比较算法,另一种为非比较算法。

  1. 比较算法有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序,希尔排序。
  2. 非比较算法有:计数排序,基数排序,桶排序。

https://gitee.com/linqiankun/Utils/tree/v3.0/

时间复杂度:

排序方法 最好情况 平均情况 最差情况 辅助空间 稳定性
冒泡排序 n n^2 n^2 1 yes
选择排序 n^2 n^2 n^2 1 no
插入排序 n n^2 n^2 1 yes
归并排序 nlogn nlogn nlogn n yes
堆排序 nlogn nlogn nlogn 1 no
快速排序 nlogn nlogn n^2 1 no
希尔排序 n^1.3 nlogn~n^2 n^2 logn~n no

比较算法:

冒泡排序

普通冒泡排序

冒泡排序是一种极为简单的排序算法。通过循环反复的遍历数组中的元素,依次比较相邻的两个元素。如果不满足排序规则,就进行位置交换,直到没有元素需要交换位置,排序完成。

这个算法会慢慢的使元素按照需要的顺序浮出来。

时间复杂度为:O(n)~O(n^2)~O(n^2)

冒泡排序运行顺序:

  1. 比较相邻元素,按照排序规则进行位置交换。
  2. 对每一对相邻元素进行第一步的操作,操作完之后,队伍最后会是满足条件的数。循环体的内层循环走完一圈。
  3. 针对所有元素做上面的操作,直到排序完成。
public static void bubbleSorted(int[] array, StatusCode statusCode) {
    //外层循环跑完,数组排序完成
    for (int i = 0; i < array.length; i++) {
        //内层循环跑完一圈,数组的最后就会增加一位已排好序的数据
        for (int j = 0; j < array.length; j++) {
            int falg = SortUtil.compare(array[i], array[j]);
            //可修改排序方向
            if (falg == statusCode.getCode()) {
                //交换数据位置
                SortUtil.swap(array, i, j);
            }
        }
    }
}

鸡尾酒排序

普通冒泡排序两层循环均不断由前到后进行比较,鸡尾酒排序进行优化,将比较顺序修改为,从前往后在从后往前不断向中间收缩的比较规则。从前往后可确定队列最后的元素为最大的,从后往前确定队列开始的元素为最小的,由此在中间结束时排序完成。

public static void CocktailSort(int[] array, int n) {
    int left = 0;                            // 初始化边界
    int right = n - 1;
    while (left < right) {
        for (int i = left; i < right; i++)   // 前半轮,将最大元素放到后面
        {
            if (array[i] > array[i + 1]) {
                SortUtil.swap(array, i, i + 1);
            }
        }
        right--;
        for (int i = right; i > left; i--)   // 后半轮,将最小元素放到前面
        {
            if (array[i - 1] > array[i]) {
                SortUtil.swap(array, i - 1, i);
            }
        }
        left++;
    }
}

选择排序

选择排序,将数组的最前端确定为已排序队列,每次每次在后面的未排序队列中选择出满足排序规则的最佳元素放置在已排序队列的末尾,直到未排序队列全部排序完成截止。

冒泡排序通过不断交换相邻数据的位置,将所有的数据移动到应该的位置。选择排序每次遍历,都可以选出需要的数据放置在合适的位置。

时间复杂度:O(n^2)~O(n^2)~O(n^2)

选择排序运行顺序:

  1. 确定最小队列位置。
  2. 从后面的未排序队列选择出最佳满足条件的放置在已排序队列的末尾。
  3. 循环遍历数组,直到所有元素排序完成结束。
public static void selectionSorted(int[] array, StatusCode statusCode) {
    //确定最小的位置
    for (int i = 0; i < array.length; i++) {
        
        int min = i;
        //确定最小的位置存放的数据
        for (int j = i + 1; j < array.length; j++) {
            //用标志为查出为排序队列中最小/大的
            if (statusCode.getCode() == SortUtil.compare(array[j], array[min])) {
                min = j;
            }
        }
        //将最小的放在最前面
        if (min != i) {
            SortUtil.swap(array, i, min);
        }
    }
}

插入排序

普通插入排序

插入排序,将数组的最前端确定为已排序队列。每次遍历选择出未排序队列的第一个元素,通过比较将其插入在已排序的队列中的合适位置。直到所有数据排序完毕。

选择排序每次从未排序队列中合适的,插入排序选出第一个插入合适的位置。选择排序比较发生在选择数据时,再未排序队列中。插入排序比较发生在已排序队列,在寻找合适的位置时。

时间复杂度:O(n)~O(n^2)~O(n^2)

执行顺序:

  1. 确定最小队列位置。
  2. 取出未排序队列的第一个元素。
  3. 将该元素与已排序队列的元素逐个比较,放入合适的位置。
  4. 继续执行,知道所有元素已排序结束。
public static void insertionSorted(int[] array, StatusCode statusCode) {
    //默认第0个是排好序的,i以前的排好序
    for (int i = 1; i < array.length; i++) {
        int get = array[i];
        int j = i - 1;
        //从当前位值倒序往前查找
        while (j >= 0 && statusCode.getCode() == SortUtil.compare(get, array[j])) {
            array[j + 1] = array[j];
            j--;
        }
        //不满足比较条件时放在位置的右边
        array[j + 1] = get;
    }

}

二分插入排序

插入排序需要在已排序队列中查找出所选的数据的合适位置,可以将查找算法修改为二分查找法,提高效率。

public static void insertionSortedDichotomy(int[] array, StatusCode statusCode) {
        for (int i = 1; i < array.length; i++) {
            int get = array[i];
            int left = 0;
            int right = i - 1;
            //二分查找数据所处的位置,也是在之前的队列中
            while (left <= right)
            {
                int mid = (left + right) / 2;
                if (statusCode.getCode() == SortUtil.compare(get,array[mid]))
//                if (array[mid] > get)
                    right = mid - 1;
                else
                    left = mid + 1;
            }
            //拿走的是位置i的数据,left之后的往后移动
            for (int j = i - 1; j >= left; j--)
            {
                array[j + 1] = array[j];
            }
            array[left] = get;
        }
    }
}

归并排序

个人认为归并排序是分治思想的一种,通过将大事化小,小事化了的思路解决问题。归并排序将两个已排序的数组合并为一个已排序的数组,完成对数组的排序。可采用递归的思想,从二二归并开始,到四四归并,八八归并,直到将所有的数据完成排序。

时间复杂度:O(nlogn)~O(nlogn)~O(nlogn)

执行顺序:

  1. 将数组长度为8的数组拆分为长度为4的数组进行归并,在通过递归将长度为4的拆分为长度为2的数组进行归并。
  2. 长度为2的归并返回之长度为4的,继续归并之长度为8的。排序结束。
  3. 任何长度的数组均按照此思路不断拆分。

归并排序

public static void mergeSortedRecursion(int[] array, StatusCode statusCode, int left, int right) {
    if (left == right)
        return;
    int mid = (left + right) / 2;
    //前半段数组归并
    mergeSortedRecursion(array, statusCode, left, mid);
    //后半段数组归并
    mergeSortedRecursion(array, statusCode, mid + 1, right);
    //归并合并
    merge(array, statusCode, left, mid, right);
}

数组合并算法

private static void merge(int[] array, StatusCode statusCode, int left, int mid, int right) {
    int len = right - left + 1;
    int[] temp = new int[len];
    int index = 0;
    //前半段数组起始
    int i = left;
    //后半段数组起始
    int j = mid + 1;
    //两组数据归并操作
    while (i <= mid && j <= right) {
        temp[index++] = statusCode.getCode() == SortUtil.compare(array[i], array[j]) ? array[i++] : array[j++];
    }
    //上面的循环无法将两个子数组的数据全部循环到
    while (i <= mid) {
        temp[index++] = array[i++];
    }
    while (j <= right) {
        temp[index++] = array[j++];
    }
    //数据放入原
    int tindex = 0;
    while (tindex < temp.length) {
        array[left++] = temp[tindex++];
    }

}

堆排序

堆排序是一种利用堆这种数据结构进行排序的算法。通过将无序的数组来构造成为大顶堆或小顶堆来进行排序。

时间复杂度:O(nlogn)~O(nlogn)~O(nlogn)

执行规则:

  1. 将输入的无序队列构造成为大顶堆。
  2. 将堆顶元素与堆尾元素互换,置换前堆顶元素为序列中最大的元素。
  3. 对置换后的堆结构进行调整,重新成为大顶堆。
  4. 循环第二步与第三步,直至堆内元素为1,排序完成。

堆排序算法

void HeapSort(int A[], int n)
{
    int heap_size = BuildHeap(A, n);    // 建立一个最大堆
    while (heap_size > 1)           // 堆(无序区)元素个数大于1,未完成排序
    {
        // 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
        // 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
        Swap(A, 0, --heap_size);
        Heapify(A, 0, heap_size);     // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
    }
}

建堆算法

int BuildHeap(int A[], int n)           // 建堆,时间复杂度O(n)
{
    int heap_size = n;
    for (int i = heap_size / 2 - 1; i >= 0; i--) // 从每一个非叶结点开始向下进行堆调整
        Heapify(A, i, heap_size);
    return heap_size;
}

堆调整算法

void Heapify(int A[], int i, int size)  // 从A[i]向下进行堆调整
{
    int left_child = 2 * i + 1;         // 左孩子索引
    int right_child = 2 * i + 2;        // 右孩子索引
    int max = i;                        // 选出当前结点与其左右孩子三者之中的最大值
    if (left_child < size && A[left_child] > A[max])
        max = left_child;
    if (right_child < size && A[right_child] > A[max])
        max = right_child;
    if (max != i)
    {
        Swap(A, i, max);                // 把当前结点和它的最大(直接)子节点进行交换
        Heapify(A, max, size);          // 递归调用,继续从当前结点向下进行堆调整
    }
}

快速排序

个人感觉快速排序是归并排序的另一种思路。通过讲一个数组根据基准数据切割为两个分区,再将分区递归进行切分的过程,知道所有数据切分完成,排序完成。

时间复杂度:O(nlogn)~O(nlogn)~O(n^2)

执行顺序:

  1. 从数组中挑选一个数据作为基准。
  2. 将大于基准数据的放置在基准的一侧,小于的放置在另一侧,形成两个分区。
  3. 对两个分区递归进行1与2的操作。
  4. 到两侧无法继续划分为止。

快速排序

public static void quickSorted(int[] array, int left, int right, StatusCode statusCode) {
    if (left >= right)
        return;
    int pivot_index = partition(array, left, right, statusCode); // 基准的索引
    //递归进行划分
    quickSorted(array, left, pivot_index - 1, statusCode);
    quickSorted(array, pivot_index + 1, right, statusCode);
}

分治块

private static int partition(int[] array, int left, int right, StatusCode statusCode)  // 划分函数
    {
        int pivot = array[right];               // 这里每次都选择最后一个元素作为基准
        int tail = left - 1;                // tail为小于基准的子数组最后一个元素的索引
        for (int i = left; i < right; i++)  // 遍历基准以外的其他元素
        {
            //会破坏稳定性
            if (SortUtil.compare(pivot, array[i]) == 0 || !(statusCode.getCode() == SortUtil.compare(pivot, array[i])))
//            if (array[i] <= pivot)              // 把小于等于基准的元素放到前一个子数组末尾
            {
                SortUtil.swap(array, ++tail, i);
            }
        }
        SortUtil.swap(array, tail + 1, right);           // 最后把基准放到前一个子数组的后边,剩下的子数组既是大于基准的子数组
        // 该操作很有可能把后面元素的稳定性打乱,所以快速排序是不稳定的排序算法
        return tail + 1;                    // 返回基准的索引
    }
}

希尔排序

通过选择不同的布长,将要排序队列进行拆分。在每一个个小组内部采用插入排序。通过不断拆分排序,将无序的队列,改变为越来越趋于有序。在布长为1时,进行最后一次排序(插入排序),完成对队列的排序。

通过希尔值不断进行拆分排序,可以降低最后一次排序的时间消耗。可以有效减少数组元素移动位置时的时间消耗。

时间复杂度:O(n^1.3)~O(nlogn)~O(n^2)~O(n^2)

执行顺序:

  1. 确定希尔值
  2. 以希尔值为步长进行直接插入排序
  3. 缩小希尔值,重复2的步骤
  4. 最后一步以希尔值为1进行直接插入排序,排序完成
public static void shellSorted(int[] array, StatusCode statusCode) {
    int shell = 0;
    //寻找最大的希尔值
    while (shell <= array.length) {
        shell = shell * 3 + 1;
    }
    //通过希尔来降低排序复杂度
    while (shell >= 1) {
        //确定希尔值后,内部使用呢直接插入排序
        for (int i = shell; i < array.length; i++) {
            int j = i - shell;
            int get = array[i];
            while (j >= 0 && statusCode.getCode() == SortUtil.compare(get, array[j])) {
                array[j + shell] = array[j];
                j = j - shell;
            }
            array[j + shell] = get;
        }
        //jian
        shell = (shell - 1) / 3;
    }
}

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部