简单排序算法分析

        本文描述的算法虽然比较简单,执行速度也相对慢一些,但仍然值得学习。因为这些简单排序算法除了比较容易理解之外,某些情况下比那些复杂的算法还要好一些。比如,对于小规模的文件以及基本有序的文件,插入排序算法能比快速算法更为有效。实际上插入排序也常作为快速排序算法实现的一部分。


一、冒泡排序

        冒泡算法运行起来非常慢,但是规则简单,因此冒泡排序算法在刚开始研究排序技术时是一个非常好的算法。

规则:
  1. 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数
  3. 针对所有的元素重复以上的步骤,除了最后一个
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
特性:
  • 不变性:末端已排定的最大元素都是有序的
  • 稳定性:稳定
示例程序:
        public void bubbleSort(int... elements) {
            for (int out = elements.length - 1; out > 0; out--) {
                for (int in = 0; in < out; in++) {
                    if (elements[in] > elements[in + 1]) {
                        int temp = elements[in];
                        elements[in] = elements[in + 1];
                        elements[in + 1] = temp;
                    }
                }
            }
        }
效率分析:

        比较次数:T1 = (N-1) + (N-2) + (N-3) +…+ 1 = N*(N-1)/2

        最多交换次数:T2 = T1 = N*(N-1)/2

        平均交换次数:T3 = T2/2 = N*(N-1)/4

        时间复杂度:T = T1 + T3 = 3*N*(N-1)/4 = O(N2)

冒泡排序的改进:

        鸡尾酒排序,也叫定向冒泡排序,是冒泡排序的一种改进。此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能。

示例程序:
        public int[] cocktailSort(int... elements) {
            int left = 0; // 初始化边界
            int right = elements.length - 1;
            while (left < right) {
                for (int i = left; i < right; i++) // 前半轮,将最大元素放到后面
                {
                    if (elements[i] > elements[i + 1]) {
                        swap(elements, i, i + 1);
                    }
                }
                right--;
                for (int i = right; i > left; i--) // 后半轮,将最小元素放到前面
                {
                    if (elements[i - 1] > elements[i]) {
                        swap(elements, i - 1, i);
                    }
                }
                left++;
            }
            return elements;
        }

        void swap(int arr[], int i, int j) {
            arr[i] = arr[i] ^ arr[j];
            arr[j] = arr[i] ^ arr[j];
            arr[i] = arr[i] ^ arr[j];
        }
        在乱数序列的状态下,鸡尾酒排序与冒泡排序的效率都很差劲。


二、选择排序

        选择排序改进了冒泡排序,将必要的交换次数从O(N2)减少到O(N)。不幸的是比较次数仍保持为O(N2)。然而,选择排序仍然为大记录量的排序提出了非常重要的改进,因为这些大量的记录需要在内存中移动,这就使交换的时间和比较的时间相比,交换时间更为重要。(在Java语言中并不是这样,Java中只是改变了引用位置,而实际对象并没有发生改变)

规则:
  1. 初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列
  2. 再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾
  3. 以此类推,直到所有元素均排序完毕
特性:
  • 不变性:已排定的起始位置的最小元素是有序的
  • 稳定性:不稳定
示例程序:
        public void selectionSort(int... elements) {
            for (int out = 0; out < elements.length; out++) {
                int min = out;
                for (int in = out + 1; in < elements.length; in++) {
                    if (elements[in] < elements[min]) {
                        min = in;
                    }
                }

                if (min != out) {
                    int temp = elements[min];
                    elements[min] = elements[out];
                    elements[out] = temp;
                }
            }
        }
    }
效率分析:

比较次数:T1 = (N-1) + (N-2) + (N-3) +…+ 1 = N*(N-1)/2

交换次数:T2 = N

时间复杂度:T = T1 + T2 = O(N2)

选择排序和冒泡排序执行了相同次数的比较,但是交换次数却是冒泡排序的1/N。所以虽然选择排序和冒泡排序的时间复杂度都是O(N2),但是选择排序无疑更快,因为它进行的交换少得多。


三、直接插入排序

        在大多数情况下,插入排序算法是本文中描述的基本排序算法中最好的一种。虽然插入算法仍然需要O(N)的时间,但是在一般情况下,它要比冒泡排序快一倍,比选择排序还要快一点。尽管它比冒泡排序算法和选择排序算法都更麻烦一些,但它也不很复杂。

规则:
  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5
特性:

不变性:有序组数据的有序性

稳定性:稳定

示例程序:
	public void insertionSort(int... elements) {
		for (int i = 1; i < elements.length; i++) {
			int j, insertion = elements[i];
			for (j = i; j > 0 && insertion < elements[j - 1]; j--) {
				elements[j] = elements[j - 1];
			}
			elements[j] = insertion;
		}
	}
效率分析:

最多比较次数:T1 = (N-1) + (N-2) + (N-3) +…+ 1 = N*(N-1)/2

平均比较次数:T2 = T1/2 = N*(N-1)/4

最少比较次数:T3 = N-1

最多移动次数:T4 = (N-1) + (N-2) + (N-3) +…+ 1 = N*(N-1)/2

平均移动次数:T5 = T4/2 = N*(N-1)/4

最少移动次数:T6 = 0


平均时间复杂度:T7 = T2  + T5 = O(N2)

最差情况时间复杂度:T8 = T1 + T4 = Ω(N2)

最好情况时间复杂度:T9 = T3 + T6 = Φ(N)


平均复制次数大致等于平均比较次数。然而一次复制与一次交换的时间耗费有所不同,所以对于随机数据,这个算法比冒泡排序快一倍,比选择排序略快。

对于已经有序或基本有序的数据来说,插入排序要好的多。然而对于逆序排列的数据,每次比较和移动都会执行,所以插入排序不比冒泡排序快。


排序方法 时间复杂度 空间复杂度 稳定性 复杂性
平均情况 最坏情况 最好情况
直接插入排序 O(N2) Ω(N2) Φ(N) O(N) 稳定 简单

直接插入算法的改进:

1.折半插入排序

        又称二分插入排序,寻找插入位置的方法采用二分法查找(也称折半查找)

2.希尔排序:

        通过交换相邻元素进行排序的任何算法,每次只减少一个逆序,平均都需要Ω(N2)时间,这个下界告诉我们,为了使一个排序算法以亚二次或O(N2)时间运行,必须执行一些比较,特别是要对相距较远的元素进行交换。一个排序算法通过删除逆序得以向前进行,而为了有效地进行,它必须使每次交换删除不止一个逆序。

        希尔排序通过比较相距一定间隔的元素来工作;各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。由于这个原因,希尔排序也叫作缩减增量排序。采用Habbard增量序列时,最坏运行时间为Ω(N3/2)。


四、有关简单排序的面试问题:

  • 为什么冒泡排序和选择排序比,时间复杂度相同,但是选择排序的效率却高于冒泡排序
  • 冒泡排序和选择排序中,还有什么更高效的交换方法(提示:通过加减法运算或位运算交换)
  • 插入排序在什么情况下,效率比冒泡排序高,什么情况下比冒泡排序效率低
  • 插入排序有什么改进方法


希望对大家有帮助,谢谢!

参考资料:

    《Java数据结构和算法(第二版)》 Robert Lafore著

    《数据结构与算法分析》 MAW著

    插入排序-百度百科

    常用排序算法总结(一)

猜你喜欢

转载自blog.csdn.net/qq_34011299/article/details/79938562
今日推荐