数据结构--Chapter7(内排序)

数据结构与算法MOOC 数据结构与算法MOOC / 第八章 内排序 练习题(Excercise for chapter8 internal sort)6:方便记忆的电话号码 数据结构与算法MOOC 数据结构与算法MOOC / 第八章 内排序 练习题(Excercise for chapter8 internal sort)6:方便记忆的电话号码AC代码解题思路 AC代码 #include <iostream> #include <string> #include <cctype> #include <map> #incl... 阅读详情

7. 内排序

7.1 排序的基本概念

1. 排序的定义

    所谓排序,简单地说就是将一组“无序”的记录序列调整为“有序”的举例序列的一种操作。通常待排序的记录有多个数据项,把用于作为排序依据的数据项称为关键字。例如,学生成绩表由学号、姓名和各科成绩等数据项组成,这些数据项都可作为关键字来进行排序。

2. 排序的分类

1)内部排序和外部排序

    按照排序过程中所涉及的存储器的不同可分为内部排序与外部排序。内部排序是指待排序序列完全存放在内存中进行的排序过程,这种方法适合数量不太大的数据元素的排序。外部排序是指待排序的数据元素非常多,以至于它们必须存储在外部存储器上,这种排序过程中需要访问外存储器,这样的排序称为外排序。

2)稳定排序与不稳定排序

    若对任意一组数据元素序列,使用某种排序算法对它进行按照关键字的排序,若相同关键字间的前后位置关系在排序前与排序后保持一致,则称此排序方法是稳定的,而不能保持一致的排序方法则称为不稳定的。例如,一组关键字序列{3,4,2,3,1},若经过排序后变为{1,2,3,3,4},则此排序方法是稳定的;若经过排序后变为{1,2,3,3,4},则此排序方法是不稳定的。

3. 内排序的方法

    内部排序的过程是一个逐步扩大记录的优先序列长度的过程。基于不同的“扩大”有序序列长度的方法,内部排序方法大致可分为以下几种类型:插入类、交换类、选择类、归并类和其他类。

1)插入类排序方法是指将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序子序列的长度。

2)交换类排序方法是指通过“交换”无序序列中的记录,从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。

3)选择类排序方法是指从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。

4)归并类排序方法是指通过“归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。

4. 排序算法的性能评价

    排序算法有很多种,在众多的排序算法中,简单地评价哪一种算法是好的很困难。通常认为某种算法是用于某些情况,而这种算法在另外情况下性能也就不如其他算法。评价排序算法好换的标准主要有两条:算法的时间复杂度和空间复杂度。

5. 待排序记录的类描述

    内部排序方法可在不同的存储结构上实现,但待排序的数据元素集合通常以线性表为主,因此存储结构多选用顺序表和链表。此外,顺序表又具有随机存取的特性,因此这儿介绍的排序算法都是针对顺序表进行操作的。

    待排序的顺序表记录类描述如下:


package chapter7;

public class RecordNode {
	public KeyType key;// 关键字
	public ElementType element;// 数据类型

	public RecordNode(KeyType key) {
		this.key = key;
	}

	public RecordNode(KeyType key, ElementType element) {
		this.key = key;
		this.element = element;
	}
}

    顺序表记录关键字类

package chapter7;

/**
 * 顺序表记录关键字类
 * 
 * @author Oner.wv
 *
 */
public class KeyType implements Comparable<KeyType> {

	public int key;// 关键字

	public KeyType() {
	}

	public KeyType(int key) {
		this.key = key;
	}

	@Override
	public String toString() {// 覆盖toString方法
		return key + " ";
	}

	@Override
	public int compareTo(KeyType another) {// 覆盖Comparable接口中比较关键字大小的方法
		int thisVal = this.key;
		int anotherVal = another.key;
		return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1));
	}
}


    顺序表记录节点类

package chapter7;

/**
 * 顺序表记录节点类
 * 
 * @author Oner.wv
 *
 */
public class ElementType {
	public String data;// 用户可自定义其他数据类型

	public ElementType() {
	}

	public ElementType(String data) {
		this.data = data;
	}

	public String toString() {
		return data;
	}

}


    待排序的顺序表类描述如下:

package chapter7;

public class SeqList {
	public RecordNode[] r;// 顺序表记录节点数组
	public int curlen;// 顺序表长度

	// 顺序表的构造方法,构造一个存储空间为maxSize个存储单元
	public SeqList(int maxSize) {
		this.r = new RecordNode[maxSize];// 为顺序表分配maxSize个存储单元
		int curlen = 0;// 置顺序表的当前长度为0
	}

	// 在当前顺序表的第i个结点之前插入一个RecordNode类型的节点x
	public void insert(int i, RecordNode x) throws Exception {
		if (curlen == r.length) {// 判断顺序表是否已满
			throw new Exception("顺序表已满");
		}
		if (i < 0 || i > curlen) {
			throw new Exception("插入位置不合理");
		}
		for (int j = curlen; j > i; j--) {
			r[i] = r[i - 1];// 插入位置及以后的数据元素后移
		}
		r[i] = x;// 插入x
		this.curlen++;// 表长度加1
	}
}


7.2 插入排序

    这儿介绍两种插入排序方法:直接插入排序和希尔排序

7.2.1 直接插入排序

    直接插入排序(Straight Insertion Sort)是一种简单地排序方法,其基本思想先将原序列分为有序区和无序区,然后再经过比较和后移操作将无序区元素插入到有序区中。

    看下面的一个例子:    
           下标: 0    1   2    3    4

初始关键字:[52] 39 67  70  52

            i=1:[39  52] 67  70 52

            i=2:[39  52  67] 70 52

            i=3:[39  52  67 70] 52

            i=4:[39  52 52 67 70]

    直接插入排序算法的主要步骤归纳如下:

1)将r[i]暂存在临时变量temp中。

2)将temp与r[j](j=i-1,i-2,...,0)依次比较,若temp.key<r[j].key,则将r[j]后移一个位置,知道temp.key>=r[j].key为止(此时j+1即为r[i]插入位置)。

3)将temp插入到第j+1个位置上。

4)令i=1,2,3,...,n-1,重复步骤1)~3)。

	// 直接插入排序
	public void insertSort() {
		RecordNode temp;
		int i, j;
		for (i = 1; i < this.curlen; i++) {// n-1趟扫描
			temp = r[i];// 将待插入的第i条记录暂存于temp中
			for (j = i - 1; j >= 0 && temp.key.compareTo(r[j].key) < 0; j--) {
				r[j + 1] = r[j];// 将前面比r[i]大的记录向后移动
			}
			r[j + 1] = temp;// r[i]插入到第j+1个位置
		}
	}


    上面算法中的“j >= 0 && temp.key.compareTo(r[i].key) < 0”语句中的“j>=0”是用来控制下标越界。为了提高算法 效率,可对该算法进行如下改进:首先将待排序的n条记录从下标为1的存储单元开始一次存放在数组r中,再将顺序表中的第0个存储单元设置为一个“监视哨”,即在查找之前把r[i]赋值给r[0],这样每循环一次只需要进行记录的比较,不需要比较下表是否越界,当比较到第0个位置时,由于“r[0].key==r[i].key”必然成立,将自动退出循环,所以只需设置一个循环条件:“ temp.key.compareTo(r[i].key) < 0”。

改进直接插入排序算法:带监视哨的直接插入排序算法

	// 带监视哨的直接插入排序
	public void insertSortWithGuard() {
		int i, j;
		for (i = 1; i < this.curlen; i++) {// n-1趟扫描
			r[0] = r[i];// 将待插入的第i条记录暂存在r[0]中,同时r[0]为监视哨
			for (j = i - 1; r[0].key.compareTo(r[j].key) < 0; j--) {// 将前面较大的数据元素向后移动
				r[j + 1] = r[j];
			}
			r[j + 1] = r[0];// r[i]插入到第j+1个位置
		}

	}


    算法性能分析:

1)空间复杂度:

从实现原理可知,直接插入排序是在原输入数组上进行后移赋值操作的(称“就地排序”),所需开辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)。

2)时间复杂度:直接插入排序耗时的操作有:比较+后移赋值。时间复杂度如下:最好情况:序列是升序排列,在这种情况下,需要进行的比较操作需(n-1)次。后移赋值操作为0次,即O(n);最坏情况:序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。后移赋值操作是比较操作的次数加上 (n-1)次,即O(n^2);渐进时间复杂度(平均时间复杂度):O(n^2)。

3)算法稳定性:直接插入排序是一种稳定的排序算法。

7.2.2 希尔排序

    希尔排序(Shell Sort),又称缩小增量排序,其原理是:先将整个序列分割成若干小的子序列,再分别对子序列进行直接插入排序,使得原来序列成为基本有序。这样通过对较小的序列进行插入排序,然后对基本有序的数列进行插入排序,能够提高插入排序算法的效率。

    看下面一个例子:


    希尔排序的主要步骤归纳如下:

1)选择一个增量序列{d0,d1,d2,...,d(k-1)}。

2)根据当前增量di将n条记录分成di个子表,每个子表中记录的下标相隔为di。

3)对各个子表中的记录进行直接插入排序

4)令i=0,1,...,k-1,重复上述步骤2)~4)。

	// 希尔排序
	public void shellSort(int[] d) {// d[]为增量数组
		RecordNode temp;
		int i, j;
		// 控制增量,增量减半,若干趟扫描
		for (int k = 0; k < d.length; k++) {
			// 一趟中若干子表,每个记录在自己所属子表内进行直接插入排序
			int dk = d[k];
			for (i = dk; i < this.curlen; i++) {
				temp = r[i];
				for (j = i - dk; j >= 0 && temp.key.compareTo(r[j].key) < 0; j -= dk) {
					r[j + dk] = r[j];
				}
				r[j + dk] = temp;
			}
		}
	}


    算法性能分析:

1)空间复杂度:

    从实现原理可知,希尔排序是在原输入数组上进行后移赋值操作的(称“就地排序”),所需开辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)

2)时间复杂度:

    希尔排序耗时的操作有:比较 + 后移赋值。时间复杂度如下:最好情况:序列是升序排列,在这种情况下,需要进行的比较操作需(n-1)次。后移赋值操作为0次,即O(n);最坏情况:O(nlog2n);渐进时间复杂度(平均时间复杂度):O(nlog2n)。

3)增量选取:希尔排序的时间复杂度与增量的选取有关,但是现今仍然没有人能找出希尔排序的精确下界。一般的选择原则是:取上一个增量的一半作为此次序列的划分增量。首次选择序列长度的一半为增量。

4)算法稳定性:

希尔排序是不稳定的。因为在进行分组时,相同元素可能分到不同组中,改变相同元素的相对顺序。

7.3 交换排序

     交换排序的基本思想是两两比较排序记录的关键字,若两个记录的次序相反则交换这两个记录,知道没有范旭的记录为止。应用交换排序基本思想的主要排序方法有冒泡排序和快速排序。

7.3.1 冒泡排序

    冒泡排序(Bubble Sort)的基本思想是将待排序的数组看成从上到下排序,把关键字值较小的记录看成“较轻的”,关键字较大的记录看成是“较重的”,较小关键字值的记录好像水中的气泡一样,向上浮;较大关键字值的记录如水中的石块向下沉,当所有气泡都浮到了相应的位置,并且所有的石头都沉到了水中,排序就结束了。

    冒泡排序算法的主要步骤如下:

1)置初值i为1。

2)在无序序列中{r[0],r[1],...,r[n-i]}中,从头至尾依次比较相邻的两个记录r[j]与r[j+1](0<=j<n-i-1),若r[j].key>r[j+1].key,则交换位置。

3)i=i+1。

4)重复步骤2)~3),直到在步骤2)中未发生记录交换或i=n-1为止。

	// 冒泡排序
	public void bubbleSort() {
		RecordNode temp;// 辅助结点
		for (int i = 1; i < this.curlen; i++) {
			for (int j = 0; j < this.curlen - i; j++) {
				if (r[j].key.compareTo(r[j + 1].key) > 0) {// 逆序时,交换
					temp = r[j];
					r[j] = r[j + 1];
					r[j + 1] = temp;
				}
			}
		}
	}


     如果上面代码中,里面一层循环在某次扫描中没有执行交换,则说明此时序列已经全部有序列,无需再扫描了。因此,增加一个标记,每次发生交换,就标记,如果某次循环完没有标记,则说明已经完成排序。

	// 优化后的冒泡排序
	public void bubbleSort2() {
		RecordNode temp;// 辅助接点
		boolean flag = true;// 是否交换的标记
		for (int i = 1; i < this.curlen && flag; i++) {// 有标记时再进行下一趟,最多n-1趟
			flag = false;// 记录未交换
			for (int j = 0; j < this.curlen - i; j++) {// 一次比较、交换
				if (r[j].key.compareTo(r[j + 1].key) > 0) {//逆序时,交换
					temp = r[j];
					r[j] = r[j + 1];
					r[j + 1] = temp;
					flag = true;
				}
			}
		}
	}


    算法性能分析:

1)空间复杂度。冒泡排序禁用了一个辅助单元,空间复杂度为O(1)。

2)时间复杂度。最好情况是排序表已有序时,在第一趟比较过程中,一次交换都未发生,所以在执行一趟排序之后就结束,这时只需要比较n-1次,不需移动记录,时间复杂度为O(n);最坏情况为逆序状态,总共要进行n-1趟冒泡排序,在第i趟排序中,比较次数为n-i,移动次数为3(n-i),则总的比较次数为0.5*n(n-1),总的移动此树为1.5n(n-1),时间复杂度为O(n^2)。

3)算法稳定性。冒泡排序是一种稳定的排序算法。

7.3.2 快速排序

    快速排序(Quick Sort)是冒泡排序的一种改进算法。快速排序采用了分支策略,即将原问题划分成若干个规模更小但与原问题相似的子问题,然后用递归方法解决这些子问题,最后再将它们组合成原问题的解。

    快速排序的基本思想是一趟排序将要排序的记录分割成独立的两个部分,其中一部分的所有记录的关键字值都比另一部分的所有关键字值小,然后再按此方法对这两部分记录分别进行快速排序,整个排序过程可以递归进行,以此达到整个记录序列变成有序。

   一趟快速排序算法的主要步骤归纳如下:

1)设置两个变量i、j,初值分别为low和high,分别表示待排序序列的起始下标和终止下标。

2)将第i个记录暂存在变量pivot中,即pivot=r[i]。

3)从下标为j的位置开始由后向前依次搜索,当找到第1个比pivot的关键字值小的记录时,则将该记录向前移动到下标为i的位置上,然后i=i+1。

4)从下标为i的位置开始由前向后依次搜索,当找到第1个比pivot的关键字值大的记录时,则将该记录向后移动到下标为j的位置上,然后j=j-1。

5)重复第3)、4)步骤,知道i==为止。

6)r[i]=pivot。

	// 快速排序
	public void quickSort() {
		qSort(0, this.curlen - 1);
	}

	// 对子表r[low..high]快速排序
	private void qSort(int low, int high) {
		if (low < high) {
			int pivotloc = Partition(low, high);// 一趟排序,                                                                                                                                
			qSort(low, pivotloc - 1);// 低子表递归排序
			qSort(pivotloc + 1, high);// 高子表递归排序
		}
	}

	// 一趟快速排序算法
	private int Partition(int i, int j) {
		RecordNode pivot = r[i];// 从第一个记录作为支点记录
		while (i < j) {
			while (i < j && pivot.key.compareTo(r[j].key) <= 0) {
				j--; // 右侧扫描
			}
			if (i < j) {
				r[i] = r[j];// 将比支点记录关键字值小的记录向前移动
				i++;
			}
			while (i < j && pivot.key.compareTo(r[i].key) > 0) {
				i++;// 左侧扫描
			}
			if (i < j) {
				r[j] = r[i];// 将比支点记录关键字值大的记录向后移动
				j--;
			}
		}
		r[i] = pivot;// 支点记录到位
		return i;// 返回支点位置
	}


    算法性能分析:

1)空间复杂度。快速排序在系统内部需要用一个栈来实现递归,最好情况,递归树的深度为log2n,其空间复杂度也就为O(logn),最坏情况,需要进行n‐1递归调用,其空间复杂度为O(n),平均情况,空间复杂度也为O(log2n)。

2)时间复杂度。在最优的情况下,快速排序算法的时间复杂度为O(nlog2n);最坏情况下,快速排序算法的时间复杂度为O(n^2);平均下来,快速排序算法的时间复杂度为O(nlog2n)。虽然快速排序的最坏时间复杂度为O(n^2),但是就平均性能而言,它是基于关键字比较的内部排序算法中速度最快的。

3)算法稳定性。快速排序是一种不稳定的排序算法。

7.4  选择排序

    选择排序的主要思想是每一趟从待拍序列中选取一个关键字值最小的记录,也即第1趟从n个记录中选取关键字值最小的记录,在第二趟中,从剩下的n-1个记录中选取关键数值最小的记录,直到每个序列中的记录都选完位置。这样,由选取记录的顺序便可得到按关键字值有序的序列。

7.4.1 直接选择排序

    直接选择排序(Straight Selection Sort)的基本思想是:每一趟从待排序序列中选择一个关键字值最小的记录,也即第1趟从n个记录中选取关键字值最小的记录,在第2趟中,从剩下的n-1个记录中选取关键字值最小的记录,直到整个序列中的记录都选完位置。这样,由选取记录的顺序便可得到按关键字值有序的序列。

	// 直接选择排序
	public void selectSort() {
		RecordNode temp;// 辅助结点
		for (int i = 0; i < this.curlen - 1; i++) {// n-1趟比较
			for (int j = i + 1; j < this.curlen; j++) {// 第i个数据和它后面的记录比较
				if (r[i].key.compareTo(r[j].key) > 0) {// 第i位置上关键字值大于j位置上关键字值大小,交换它们
					temp = r[j];
					r[j] = r[i];
					r[i] = temp;
				}
			}
		}
	}

	// 改进上面的算法
	// 每次找到最小的数据的索引,减少交换的次数
	public void selectSort2() {
		RecordNode temp;// 辅助结点
		for (int i = 0; i < this.curlen - 1; i++) {// n-1趟比较
			int min = i;// 设第i条记录的关键字值最小
			for (int j = i + 1; j < this.curlen; j++) {// 第i个数据和它后面的记录比较
				if (r[min].key.compareTo(r[j].key) > 0) {
					min = j;
				}
			}
			if (min != i) {// 每趟比较最多交换一次 ,将本趟关键字值最小的记录与第i条记录交换
				temp = r[i];
				r[i] = r[min];
				r[min] = temp;
			}
		}
	}

    

    算法性能分析:

1)空间复杂度。直接选择排序仅用了一个辅助单元,空间复杂度为O(1)。

2)时间复杂度。直接选择排序的时间复杂度为O(n^2)。

3)算法稳定性。直接选择排序是一种不稳定的排序算法。

7.4.2  树形选择排序

    树形选择排序(Tree Selection Sort)又称锦标赛排序(Tournament Sort),它的原理:首先对n个记录的关键字进行两两比较,比较的结果是把关键字值较小者作为优胜者上升到父结点,得到[n/2](向上取整)个比较的优胜者,然后对这[n/2](向上取整)个记录再进行关键字的两两比较,如此重复,直到选出一个关键字值最小的记录为止。

    这个过程可以用一棵有n个叶子结点的完全二叉树表示。如图中的二叉树表示从8个关键字中选出最小关键字的过程:


    8个叶子结点中依次存放排序之前的8个关键字,每个非终端结点中的关键字均等于其左、右孩子结点中较小的那个关键字,则根结点中的关键字为叶子结点中的最小关键字。

    在输出最小关键字之后,根据关系的可传递性,欲选出次小关键字,仅需将叶子结点中的最小关键字(13)改为“∞”,然后从该叶子结点开始,和其左右兄弟的关键字进行比较,修改从叶子结点到根结点的路径上各结点的关键字,则根结点的关键字即为次小值。


    同理,可依次选出从小到大的所有关键字。

    由于含有n个叶子结点的完全二叉树的深度为[log2n]+1,则在树形选择排序中,除了最小关键字以外,每选择一个次小关键字仅需进行[log2n]次比较,因此,它的时间复杂度为O(nlog2n)。

    但是,这种排序方法也有一些缺点,比如辅助存储空间较多,并且需要和“最大值”进行多余的比较。    为了弥补,另一种选择排序被提出——堆排序。

7.4.3  堆排序

    堆排序(Heap Sort)是一种重要的选择排序方法,它只需要一个记录大小的辅助存储空间,每个待排序的记录仅占用一个记录大小的存储空间,因此弥补了树形选择排序的弱点。

1. 堆的定义

    n个记录关键字的序列{k0,k1,..kn-1},当且仅当满足下面的关系式①或②时称为堆(Heap)。

    ki<=k2i+1且ki<=k2i+2,其中2i+1<=n-1,2i+2<=n-1   ①

    ki>=k2i+1且ki>=k2i+2,其中2i+1<=n-1,2i+2<=n-1    ②

前者称为小顶堆,后者称为大顶堆。例如:关键字序列{12,36,24,85,47,30,53,91}是一个小顶堆;关键字序列{91,47,85,24,36,53,30,16}是一个大顶堆。

    采用一个数组存储序列{k0,k1,...,kn-1},则该序列可以看作是一棵顺序存储的完全二叉树,那么ki和k2i+1、k2i+2的关系就是双亲与其左、右孩子之间的关系。因此,通常用完全二叉树的形式来直观地描述一个堆。i结点的父结点下标为(i-1)/2,i结点的左右子结点下标分别为2*i+1和2*i+2。

    如最大化堆存储如下:


    实现堆排序需要解决两个问题:

1)如何把一个无序序列建成一个堆?

2)如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?

    先考虑第2)个问题。一般在输出堆顶元素之后,视为将这个元素排除,然后用表中最后一个元素填补它的位置,自上向下进行调整:首先将堆顶元素和它的左右子树的根结点进行比较,把最小的元素交换到堆顶;然后顺着被破坏的路径一路调整下去,直至叶子结点,就得到新的堆。这种从堆顶到叶子结点的调整过程也称为“筛选”。

2.  建初始堆

   从无序序列建立堆的过程就是一个反复“筛选”的过程。因为完全二叉树的最后一个非叶子结点的编号是[n/2-1]向下取整,所以“筛选”只需要从编号是n/2-1]向下取整所得到的数的结点开始。如下图,就是把无序序列{16,7,3,20,17,8}建成一个大顶堆的过程。

1)首先根据该序列元素构建一个完全二叉树


2)然后需要构造初始堆,则从最后一个非叶节点开始调整,调整过程如下:


3)20和16交换后导致16不满足大顶堆的性质,因此需重新调整


3. 筛选法调整堆

    当将堆顶最大关键字值的结点与堆最后一个结点(第n-1个结点)交换后,交换后的根结点与其左右子树可能不符合堆的定义。此时,需要调整根结点与其左右两个堆顶结点之间的大小关系,使之符合堆的定义。调整方法如下:

1)将根结点r[0]与左、右子树中关键字值较大的结点进行交换。

2)当与左孩子交换,则左子树的堆被破坏,且仅左子树的根结点不满足堆的性质;若与右孩子交换,则右子树的堆被破坏,且仅右子树的根结点不满足堆的性质。对不满足堆性质的子树继续进行上述交换操作,直到叶子结点或者堆被建成为止。如下图,就是一次筛选法调整堆的过程。

1)上面已经建好了一个大顶堆。


2)将堆顶元素20和最后一个结点3(第n-1个结点)交换。


3)此时3位于堆顶不满堆的性质,则需调整继续调整

4.  堆排序

    堆排序是一种选择排序。建立的初始堆为初始的无序区。进行堆排序时,首先输出堆顶结点(因为它的关键字值是最大的),然后将堆顶结点和最后一个结点(第n-1个结点)交换,这样,第n-1个位置作为有序区,前n-2个位置仍是无序区,对无序区进行筛选法调整堆,调整后得到新的大顶堆后,再将堆顶结点(因为它的关键字值是次大的)和最后第2个结点(第n-2个结点)进行交换,这样有序区长度变为2,然后再对无序区进行调整构成新的大顶堆,每次交换都导致无序区的长度减1,有序区的长度加1。如此反复,直到整个序列有序,则堆排序完成。下面演示了一个堆排序的过程。

1)首先先将堆顶结点和最后一个结点进行交换,然后重新调成一个新的大顶堆,即上面筛选法调整堆中所演示的案例,调整后的结果如下:


2)将调整后生成的堆顶结点17和最后第2个结点3(第n-2个结点)进行交换


3)调整后发现3不满足堆的定义,然后进行调整


4)继续将堆顶结点和无序区中的最后一个结点进行交换,并进行调整,直到整个序列都有序为止。


实现代码如下:

	// 堆排序
	public void heapSort() {
		RecordNode temp;
		for (int i = curlen / 2 - 1; i >= 0; i--) {// 创建堆
			heapAdjust(i, curlen);
		}
		for (int i = curlen - 1; i > 0; i--) {// 每趟将最大关键字值交换到后面,再调整成堆
			temp = r[0];
			r[0] = r[i];
			r[i] = temp;
			heapAdjust(0, i);
		}
	}

	// 筛选调整堆算法
	private void heapAdjust(int low, int high) {
		int i = low;// 子树的根结点
		int j = 2 * i + 1;// j为i结点的左孩子
		RecordNode temp = r[i];
		while (j < high) {
			if (j < high - 1 && r[j].key.compareTo(r[j + 1].key) < 0) {//
				j++;// 记录比较,j为左右孩子的较大者
			}
			if (temp.key.compareTo(r[j].key) < 0) {// 若父母结点值较小
				r[i] = r[j];// 孩子结点中的较大值上移
				i = j;
				j = 2 * i + 1;
			} else {
				j = high + 1;// 退出循环
			}
		}
		r[i] = temp;// 当前子树的原根植调整后的位置
	}


    算法性能分析:

1)空间复杂度。堆排序需要一个记录的辅助存储空间,空间复杂度为O(1)。

2)时间复杂度。堆排序的时间复杂度为O(nlog2n)。

3)算法稳定性。堆排序是一种不稳定的排序算法。

7.5 归并排序

    归并排序(Merging Sort)是与插入排序、交换排序、选择排序不同的另一类排序方法。归并的含义是将两个或两个以上的有序表合并成一个新的有序表。其中,将两个有序表合并成一个有序表的归并排序称为二路归并排序,否则称为多路归并排序。归并排序既可用于内部排序,也可以用于外部排序,这里仅对内部排序的二路归并方法进行讨论。

示例:

	// 归并排序
	public void mergeSort() {
		mergepass(r, 0, this.curlen - 1);
	}
	
	/**
	 * 将数组r中的r[low..high]进行归并排序
	 * 
	 * @param r
	 *            待排序的数组
	 * @param low
	 *            待排序数组的起始角标
	 * @param high
	 *            待排序数组的结束角标
	 */
	private void mergepass(RecordNode[] r, int low, int high) {
		if (low < high) {
			int mid = (low + high) / 2;
			mergepass(r, low, mid);// 递归左半部分处理
			mergepass(r, mid + 1, high);// 递归右半部分处理
			merge(r, low, mid, high);// 左右半边归并
		}
	}

	// 两个相邻的有序序列的归并算法
	/**
	 * 将数组r中的r[low..mid]和r[mid+1..high]进行归并
	 * 
	 * @param r
	 *            待排序的数组
	 * @param low
	 *            待排序数组中第一段的起始角标
	 * @param mid
	 *            待排序数组中第一段的结束角标
	 * @param high
	 *            待排序数组中第二段的结束角标
	 */
	private void merge(RecordNode[] r, int low, int mid, int high) {
		int i = low, j = mid + 1, k = 0; // mid+1为第二段有序子序列第一个元素角标
		RecordNode[] temp = new RecordNode[high - low + 1];// 辅助数组
		while (i <= mid && j <= high)// 顺序选取两个有序子序列中的较小元素,存储到辅助数组temp中
		{
			if (r[i].key.compareTo(r[j].key) <= 0)// 将较小的存入辅助数组temp中
				temp[k++] = r[i++];
			else
				temp[k++] = r[j++];
		}
		while (i <= mid) {// 若比较完之后,第一个有序子序列仍有剩余,则直接复制到temp数组中
			temp[k++] = r[i++];
		}
		while (j <= high) {// 若比较完之后,第二个有序子序列仍有剩余,则直接复制到temp数组中
			temp[k++] = r[j++];
		}
		for (i = low, k = 0; i <= high; i++, k++) {// 将归并后排好序的数据复制到r中的low~high区间
			r[i] = temp[k];
		}
	}


    算法性能分析:

1)空间复杂度。二路归并排序需要与一个待排序记录序列等长的辅助数组来存放排序过程中的中间结果,所以空间复杂度为O(n)。

2)时间复杂度。二路归并排序时间复杂度为O(nlog2n)。

3)算法稳定性。归并排序是一种稳定的排序算法。

7.6  基数排序

    基数排序(Radix Sort)是一种借助于多关键字进行排序,也是一种将但关键字按基数分成“多关键字”进行排序的方法。

7.7 各种内部排序算法的性能比较

    各种内部排序算法的性能比较如下表:


     下面给出几种选择排序算法的建议:

1)若n较小(例如n<=50),则可采用直接插入排序或直接选择排序。

2)若记录序列初始状态基本有序(指正序),则应采用直接插入排序、冒泡排序。

3)若n较大时,则应采用时间复杂度为O(nlog2n)的排序算法:快速排序、堆排序或归并排序。

    快速排序是目前基于比较的内部排序中被认为最好的一种排序算法,当待排序的关键字是随机分布时,快速排序的平均时间最短;堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况(当待排记录序列按关键字排序基本有序时),这两种排序都是不稳定的;当内存空间允许,且要求排序时稳定时,则可选用归并排序。

【数据结构】什么是内排序、外排序?内排序详细介绍 内部排序是指待排序列数据记录完全存放在内存中所进行的排序过程。 外部排序在排序过程中还需要访问外部存储器的排序。 阅读详情

相关推荐

【数据结构】插入排序 — 直接插入排序

​直接插入排序是一种最简单的排序方法,其基本操作是将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表。

四季轮换叶,一路招摇胜 1万+

数据结构实验课_实验一 顺序表

一、实验内容 1.输入一组整型元素序列,建立顺序表。 2.遍历该顺序表。 3.在该顺序表中进行顺序查找某一元素,查找成功返回1,否则返回0。 4.实现把该表中所有奇数排在偶数之前,即表的前面为奇数,后面为偶数。 5.判断该顺序表中元素是否对称,对称返回1,否则返回0。 6.输入整型元素序列利用有序表插入算法建立一个有序表。 代码实现 #include <stdio.h> #include <stdlib.h> #include <time.h> #define MAXSI

zavay的博客 3826

顺序表实现

Java顺序表的学习实现代码,支持多种数据类型的临时存储

java基础-插入排序

小伙伴们,你们好呀!我是老寇! package sort; public class SortTest { public static void main(String[] args) throws Exception{ int[] d={52, 39, 67, 95, 70, 8, 25, 52}; SeqList seqList=new SeqList(20); for (int i = 0; i < d.length; i++) { RecordNode r=n

. 3万+

插入法,把一个数插到有序数列中,插入后数列仍然有序,VB6.0源代码

插入法,把一个数插到有序数列中,插入后数列仍然有序,VB6.0源代码

LangGraph实战:用Plan-and-Execute模式5步搭建智能问答机器人(附Tavily搜索集成)

本文详细介绍了如何利用LangGraph的Plan-and-Execute模式,通过规划、执行、动态调整三个核心阶段,构建一个能够处理复杂多步骤问题的智能问答机器人。文章提供了从环境配置、状态管理到核心节点(规划、执行、重新规划)实现的完整实战指南,并集成了Tavily搜索工具,展示了如何将智能任务规划应用于实际信息查询场景。

weixin_29258297的博客 339

Java六种排序算法

一、建立顺序表节点的类 package sorts; public class RecordNode { private Comparable key; //关键字 private Object element; //数据元素 public Comparable getKey() { return key; } public void setKey(C

Shava_Shaw 720

基于Arduino 的 FM 调频收音机

这是一个基于 Arduino 的开源的收音机项目,它具备一台标准收音机的全部功能,从控制部分到外壳都可以自己 DIY。在项目中,我们将介绍让 FM 收音机调谐器的 RDA5807 模块与 I2C 协议进行通信的方法,整个过程只需要两根线就可以与 IC 通信,接线非常简单。3D 打印的外壳也非常简洁。总之,是个非常简单和实用的项目,来试试吧!

ByteBox的博客 1641

插入排序算法四种实现

插入排序算法实现 直接插入排序不带哨兵版本 void InsertSort(int a[],int n){ //对第二个元素到最后一个元素进行插入操作 int i,j,t; for(i=1;i<n;i++){ int t=a[i]; //通过对比前面的元素找到插入位置 for(j=i-1;j>=0&&a[j]>t;j--){ a[j+1] = a[j]; } //到j+1的位置进行插入 a[j+1]=t; } } 直接

weixin_43752167的博客 662

高通Camera PDAF调试实战:从Type2到Type3的避坑指南与Log分析技巧

本文深入解析高通Camera平台PDAF调试实战,重点对比Type2与Type3的核心差异,提供详细的配置迁移方案和Log分析技巧。针对常见兼容性问题如对焦速度变慢、低光失效等,给出具体解决方案,并分享性能优化案例与高级调试工具链,帮助开发者快速提升PDAF调试效率。

weixin_29164185的博客 409

SystemRDL:从寄存器描述到自动化生成的完整指南

本文详细介绍了SystemRDL如何通过“一份描述,多处生成”的理念,彻底解决芯片与FPGA设计中手动维护寄存器带来的不一致与低效问题。文章提供了从语言快速上手、核心组件讲解到使用PeakRDL工具链实现自动化代码与文档生成的完整实战指南,帮助硬件工程师构建高效、可靠的寄存器开发流程。

weixin_29243279的博客 656

尝试用顺序表实现排序算法(持续更新..)

数据结构刚刚学到顺序表,于是顺便试下复习下C语言,自学下后面必会的各种排序算法并自己实现下。(排序算法毕竟比较基础也是很多时候面试会面到的)算是随笔吧,最近又挺忙的,搞项目的事 排序算法很多,按依据的原则分可以有:**- 插入排序 - 交换排序 - 选择排序 - 归并排序 - 计数排序**按时间复杂度分可以有: O(nlogn); O(n^2); O(d*n);1. 插入

erichhhhho的博客 1万+

【反封禁策略】集成 Undetected-Chromedriver:让 Browser-Use 绕过 Cloudflare 五秒盾

摘要:AI Agent如何突破Cloudflare检测的实战指南 本文针对AI Agent在Cloudflare检测下频繁受阻的问题,深入分析了undetected-chromedriver的工作原理与局限性。文章指出,Cloudflare 2026年的检测体系已升级为"浏览器指纹+IP信誉+协议指纹+行为模式"的多维交叉验证,单一伪装方案难以奏效。 核心内容包括: undetected-chromedriver的三层补丁体系解析 v2与v3版本的适用场景对比 两种Browser-Use集成方案:Selen

ZDQ58818的博客 284

实验七--查找算法

一、    实验目的: 1.  熟练掌握常用的顺序查找法,折半查找法等查找算法。 2.  深入理解各种查找算法的结构特点及各算法之间的区别。 二、    实验内容: 1.常用的一些查找算法。 2.各种查找算法的结构特点及各算法之间的区别。 三、     实验要求: 阅读以下四个类: RecordNode.java SeqList_Sort.java Searc

Unitue_逆流 1328

使用 SAP 自家的 AI 工具 Joule 辅助 SAP Fiori 应用开发

SAP Joule 不请自答,解释说标记了接口的 UI Component,具备进行异步内容创建的能力,比如其通过 rootView 分配的视图,在运行时可以异步创建。目前 SAP Business Application Studio 里的 SAP Joule 支持下面这八种上下文,绝大部分都和 SAP UI5 开发相关,剩下的则是代码仓库和 SAP 帮助文档的搜索。SAP Joule 回答的大意是:我只负责回答 SAP UI5 开发相关的问题,不要和我扯其他有的没的。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 1093

C#单例模式在工控上位机的实战应用:一台设备只连一次,稳定运行7×24小时

在工控行业,稳定性比功能更重要。一台上位机需要7×24小时运行,任何一次因资源冲突导致的停机,都可能给工厂带来巨大损失。单例模式的核心价值,就是通过保证资源唯一性,避免PLC连接冲突、串口占用、日志写入错乱等问题,为工控上位机的稳定运行保驾护航。按需使用:不是所有类都要单例,只有资源唯一性的类才用;线程安全:多线程场景下,必须用双重检查锁定或静态内部类写法;手动释放:单例类中的硬件资源(PLC、串口),程序退出时必须手动释放,避免资源泄漏。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 549

java数据结构折半查找和二叉排序树

一、实验目的 1、掌握查找的特点。 2、掌握折半查找的基本思想及其算法。 3、熟悉二叉排序树的特点,掌握二叉排序树的插入、删除操作。 二、实验内容 1、设有关键字序列k={ 5 ,14 ,18 ,21 ,23 ,29 ,31 ,35 },查找key=21和key=25的数据元素。 2、根据关键字序列{45、24、53、12、37、93}构造二叉排序树,并完成插入13删除关键字53和24的操作。 三...

sticker_start_tag的博客 1164

Debian Buster 软件源失效问题

摘要:基于Debian Buster的ARM64设备因系统版本结束支持(EOL)导致apt update报错404。解决方案包括:1)改用Debian官方存档源(需禁用安全检查);2)升级至受支持的Debian 11/12版本(推荐);3)临时使用第三方镜像源(不推荐)。操作步骤涉及修改sources.list、处理NTP同步问题及清理残留配置。建议优先采用版本升级方案以获得持续安全更新,避免使用过时系统带来的安全隐患。文中提供了完整命令示例和操作记录供参考。

yy_xzz的博客 2178

7-133 简化的插入排序 (15 分)

本题要求编写程序,将一个给定的整数插到原本有序的整数序列中,使结果序列仍然有序。 输入格式: 输入在第一行先给出非负整数N(<10);第二行给出N个从小到大排好顺序的整数;第三行给出一个整数X。 输出格式: 在一行内输出将X插入后仍然从小到大有序的整数序列,每个数字后面有一个空格。 输入样例: 5 1 2 4 5 7 3 输出样例: 1 2 3 4 5 7 ...

qq_57321225的博客 250

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

十大经典排序算法欢聚一堂

1.常见排序算法分类 十种常见排序算法一般分为以下几种: (1)非线性时间比较类排序:交换类排序(快速排序和冒泡排序)、插入类排序(简单插入排序和希尔排序)、选择类排序(简单选择排序和堆排序)、归并排序(二路归并排序和多路归并排序); (2)线性时间非比较类排序:计数排序、基数排序和桶排序。 总结: (1)在比较类排序中,归并排序最快,其次是快速排序和堆排序,两者不相伯仲,但是有一点需要..................

Doublelv 的博客专栏。 3万+

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

冒泡排序、插入排序、选择排序、希尔排序、堆排序、归并排序等常用排序算法的比较

掌握好常用的排序算法,在实际的项目开发中可以节省很多的时间。每一种排序算法在执行的效率上是存在差别的,这些微小的时间差,也许在平常的联系当中感觉不到,但是涉及到数据量比较大或者是在资源比较紧张的系统中就显得尤其的重要,比如嵌入式系统。下面简要介绍三种常用的排序算法以及他们的执行效率的比较。        冒泡排序:最优为O(n),最坏为O(n^2),平均O(n^2)        思路:将相邻

Invoker's Tower 9932

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

五类排序算法(插入,交换,选择,归并)

五种排序方法 插入排序 直接插入排序 希尔排序 交换排序 冒泡排序 快速排序 选择排序 简单选择排序 堆排序 归并排序 分类排序 排序方法基本思想和算法描述 直接插入排序/冒泡排序/简单选择排序,这些简单算法所需时间复杂度大为O(n^2). 希尔排序/快速排序/堆排序/归并排序,这些较复杂算法的时间复杂度,平均情况下位O(nlog2n),有些最快情况下退化成O(n^2).

biezhihua的专栏 1万+

实验7-1-1 简化的插入排序 (15分)

实验7-1-1 简化的插入排序 (15分) 本题要求编写程序,将一个给定的整数插到原本有序的整数序列中,使结果序列仍然有序。 输入格式: 输入在第一行先给出非负整数N(<10);第二行给出N个从小到大排好顺序的整数;第三行给出一个整数X。 输出格式: 在一行内输出将X插入后仍然从小到大有序的整数序列,每个数字后面有一个空格。 输入样例: 5 1 2 4 5 7 3 输出样例: 1 2 3 4 5 7 #include<stdio.h> int main() { int

一只大番茄的博客 2411

排序-----直接插入排序------带监视哨和不带监视哨

源代码 public class Main { public static void main(String[] args) { int[] a={20,30,21,35,2,1}; //insertSort(a); insertSortWithLookOut(a); for(int i=0;i&lt;a.length;i++...

weixin_42706227的博客 1620

各种排序算法(内部排序)及其实现

本文是基于《数据结构(C语言版)(第二版)》(严蔚敏)其排序章节所做的总结。因此具体解释可以去参考此书。概念什么是排序排序是按关键字的非递减或非递增顺序对一组记录重新进行排列的操作。数学描述: 设n个记录的序列为 {R1, R2, …, Rn},其关键字序列为 {K1, K2, …, Kn} 确定1, 2, …, n的一种排序p1, p2, …, pn,使之满足Kp1 <= Kp2 <= …

luoluozlb 4932

实验八--排序算法

实验八 排序算法的实现 一、 实验目的: 1. 熟练掌握常用的直接插入排序、简单选择排序、冒泡排序等算法。 2. 深入理解各种查找排序的结构特点及各算法之间的区别。 二、 实验内容: 1. 采用直接插入排序、简单选择排序、冒泡排序等常用的排序算法实现给数据的排序。 2. 各种排序算法的结构特点及各算法之间的区别。 代码如下: RecordNode.javapa

Unitue_逆流 1814
上一篇: 数据结构--Chapter6(图)
下一篇: 数据结构--Chapter8(外排序)
Wang_AI
博客等级 码龄12年 988粉丝 162原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值