数据结构与算法笔记(3) 排序算法基础
1.什么是排序
排序就是将一个数据元素(或记录)的任意序列,通过一定的方法重新排列成一个按关键字有序的序列的过程。
2.排序的稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
稳定的排序的优点是,从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。
例如,假设一个班的学生已经按照学号大小排好序了,我现在要求按照年龄从小到大再排个序,如果年龄相同的,必须按照学号从小到大的顺序排列。
那么问题来了,你选择的年龄排序方法如果是不稳定的,是不是排序完了后年龄相同的一组学生学号就乱了,你就得把这组年龄相同的学生再按照学号拍一遍。如果是稳定的排序算法,我就只需要按照年龄排一遍就好了。
3.内部排序和外部排序
根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为内部排序和外部排序。
-
内部排序:在整个排序过程中,待排序的所有记录全部被放置在内存中。
-
外部排序:由于排序的记录个数太多,不能同时放置在内存中,整个排序过程需要在内外存之间多次交换数据才能进行。
4.内部排序方法
根据排序过程中借助的主要操作,我们把内部排序分为:插入排序、交换排序、分配排序、选择排序和归并排序等。
4.1.插入排序
插入排序的基本方法是:每步将一个待排序的记录按照其排序码的大小,插入到前面已经排序的文件中的适当位置,直到全部插入完为止。插入排序算法主要包括直接插入排序、二分插入排序、表插入排序和希尔排序等。
4.1.1.直接插入排序
直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
具体算法描述如下:
-
从第一个元素开始,该元素可以认为已经被排序
-
取出下一个元素,在已经排序的元素序列中从后向前扫描
-
如果该元素(已排序)大于新元素,将该元素移到下一位置
-
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
-
将新元素插入到该位置后
-
重复步骤2~5
#include<stdio.h> #define MAXNUM 100 typedef int KeyType; typedef int DataType; typedef struct { KeyType key; /* 排序码字段 */ /*DataType info; 记录的其它字段 */ } RecordNode; typedef struct { int n; /* n为文件中的记录个数,n<MAXNUM */ RecordNode record[MAXNUM]; } SortObject; void insertSort(SortObject * pvector) { /* 按递增序进行直接插入排序 */ int i, j; RecordNode temp; RecordNode *data = pvector->record; for( i = 1; i < pvector->n; i++ ) { /* 依次插入记录R1, R2…Rn-1 */ temp = data[i]; for ( j = i-1; temp.key < data[j].key && j >= 0; j-- ) /* 由后向前找插入位置 将排序码大于ki的记录后移 */ data[j+1] = data[j]; if( j != i-1 ) data[j+1] = temp; } } SortObject vector = {10, 49, 38, 65, 97, 76, 13, 27, 49, 50, 101}; int main(){ int i; insertSort(&vector); for(i = 0; i < vector.n; i++) printf("%d ", vector.record[i]); getchar(); return 0; }
4.1.2.二分插入排序
直接插入排序的算法简洁,容易实现,n较小时是一种很好的排序算法。但是,因为如果文件中记录的数量很大,则此时直接插入排序方法不太适用。在直接插入排序的基础上,为了减少比较的次数,可以在插入第i个元素时改用二分比较法查找插入位置,便得到了二分插入排序。
所谓的二分比较法是在插入第i+1个元素时(前i个元素已排序),取第i/2位置的排序码与要插入的元素比较,如果要插入的元素小于第i/2位置的排序码,则在第1到第i/2位置之间使用二分法,反之则在第i/2+1到第i位置之间使用二分法,直到确定插入位置为止。这种方法经过一次比较,便可排除一半记录,故称为二分法。
#include<stdio.h> #define MAXNUM 100 typedef int KeyType; typedef int DataType; typedef struct { KeyType key; /* 排序码字段 */ /*DataType info; 记录的其它字段 */ } RecordNode; typedef struct { int n; /* n为文件中的记录个数,n<MAXNUM */ RecordNode record[MAXNUM]; } SortObject; void binSort(SortObject * pvector) { /* 按递增序进行二分法插入排序 */ int i, j, left, mid, right; RecordNode temp; RecordNode *data = pvector->record; for( i = 1; i < pvector->n; i++ ) { temp = data[i]; left = 0; right = i-1; /* 置已排序区间的下、上界初值 */ while (left <= right) { mid = (left + right)/2; /* mid指向已排序区间的中间位置 */ if (temp.key < data[mid].key) right = mid-1; /* 插入元素应在左子区间 */ else left = mid+1; /* 插入元素应在右子区间 */ } for (j = i-1; j >= left; j--) data[j+1] = data[j]; /* 将排序码大于ki的记录后移 */ if (left != i) data[left] = temp; } } SortObject vector={10, 49,38,65,97,76,13,27,49,50,101}; int main(){ int i; binSort(&vector); for(i = 0; i < vector.n; i++) printf("%d ", vector.record[i]); getchar(); return 0; }
4.1.3.表插入排序
表插入排序的目标是在直接插入排序的基础上减少记录移动的次数,其基本的思想是在每个记录中增加一个指针字段,指向下一个记录。整个被排序的文件表示成一个记录的单链表。插入记录i时,记录0到i-1已经排序了,为了确定插入的位置,可以先将记录i脱链,再采用顺序比较的方法找到i应当插入的位置,像单链表插入那样,将i插入链表。
#include<stdio.h> struct Node; /* 单链表结点类型 */ typedef int KeyType; typedef int DataType; typedef struct Node ListNode; struct Node { KeyType key; /* 排序码字段 */ /*DataType info; 记录的其它字段 */ ListNode *next; /* 记录的指针字段 */ }; typedef ListNode * LinkList; /* 对链表按递增序进行表插入排序,链表中第一个结点为表头结点。 */ void listSort(LinkList * plist) { ListNode *now, *pre, *p, *q, *head; head=*plist; if (head->next == NULL || head->next->next == NULL) return; /* 为空链表或链表中只有一个结点 */ pre = head->next; now = pre->next; while (now != NULL) { q = head; p = head->next; while(p != now && p->key <= now->key) { q = p; p = p->next; } /* 本循环结束时,已经找到了now的插入位置 */ if (p == now) { /* now应放在原位置 */ pre = pre->next; now = pre->next; continue; } /* 使now记录脱链,将now记录插入链表中 */ pre->next = now->next; q->next = now; now->next = p; now = pre->next; } } ListNode element[9]={ 0, &element[1], 49, &element[2], 38, &element[3], 65, &element[4], 97, &element[5], 76, &element[6], 13, &element[7], 27, &element[8], 49, NULL }; int main(){ LinkList p = element; listSort(&p); p = p->next; while (p != NULL){ printf("%d ",p->key); p = p->next; } getchar(); return 0; }
4.1.4.希尔排序
希尔排序的方法又称缩小增量法,是对直接插入排序法的改进。它的做法是:先取一个整数d1<n,把全部记录分成d1个组,所有距离为d1倍数的记录放在一个组中,先在各组内排序;然后取d2<d1,重复上述分组和排序工作;直到di=1,即所有的记录放在一个组中为止。各组内的排序可以采用直接插入排序或其他排序。
#include<stdio.h> #define MAXNUM 100 typedef int KeyType; typedef int DataType; typedef struct { KeyType key; /* 排序码字段 */ /*DataType info; 记录的其它字段 */ } RecordNode; typedef struct { int n; /* n为文件中的记录个数,n<MAXNUM */ RecordNode record[MAXNUM]; } SortObject; void shellSort(SortObject * pvector, int d) { /* 按递增序进行Shell排序 */ int i, j, inc; RecordNode temp, *data = pvector->record; for (inc = d; inc > 0; inc /= 2) { /* inc 为本趟shell排序增量 */ for (i = inc; i < pvector->n; i++) { temp = data[i]; /* 保存待插入记录Ri*/ for (j = i-inc; j >= 0 && temp.key < data[j].key; j -= inc) data[j+inc] = data[j]; /* 查找插入位置,记录后移 */ data[j+inc] = temp; /* 插入记录Ri */ } } } SortObject vector={8,49,38,65,97,76,13,27,49}; int main(){ int i; shellSort(&vector,4); for(i=0;i<8;i++) printf("%d ",vector.record[i]); getchar(); return 0; }
4.2.交换排序
交换排序的基本方法是:两两比较待排序记录的排序码,交换不满足顺序要求的偶对,直到全部满足为止。
交换排序主要包含冒泡排序和快速排序算法
4.2.1.冒泡排序
冒泡排序的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
#include<stdio.h> #define MAXNUM 100 #define TRUE 1 #define FALSE 0 typedef int KeyType; typedef int DataType; typedef struct { KeyType key; /* 排序码字段 */ /*DataType info; 记录的其它字段 */ } RecordNode; typedef struct { int n; /* n为文件中的记录个数,n<MAXNUM */ RecordNode record[MAXNUM]; } SortObject; void bubbleSort(SortObject * pvector) { int i, j, noswap; RecordNode temp, *data = pvector->record; for(i = 0; i < pvector->n-1; i++) { /* 做n-1次起泡 */ noswap = TRUE; /* 置交换标志 */ for (j = 0; j < pvector->n-i-1; j++) /* 从前向后扫描 */ if (data[j+1].key < data[j].key) { /* 交换记录 */ temp = data[j]; data[j] = data[j+1]; data[j+1] = temp; noswap = FALSE; } if ( noswap ) break; /* 本趟起泡未发生记录交换,算法结束 */ } } SortObject vector={8, 49,38,65,97,76,13,27,49}; int main(){ int i; bubbleSort(&vector); for(i = 0; i < 8; i++) printf("%d ", vector.record[i]); getchar(); return 0; }
4.2.2.快速排序
快速排序是一种十分重要的排序算法,它的基本思想是:通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
#include<stdio.h> #define MAXNUM 100 #define TRUE 1 #define FALSE 0 typedef int KeyType; typedef int DataType; typedef struct { KeyType key; /* 排序码字段 */ /*DataType info; 记录的其它字段 */ } RecordNode; typedef struct { int n; /* n为文件中的记录个数,n<MAXNUM */ RecordNode record[MAXNUM]; } SortObject; void quickSort(SortObject * pvector, int l, int r) { int i, j; RecordNode temp, *data = pvector->record; if (l >= r) return; /* 只有一个记录或无记录,则无须排序 */ i = l; j = r; temp = data[i]; while (i != j) { /* 寻找Rl的最终位置 */ while( data[j].key >= temp.key && j > i ) j--; /* 从右向左扫描,查找第1个排序码小于temp.key的记录 */ if (i < j) data[i++] = data[j]; while( data[i].key <= temp.key && j > i ) i++; /* 从左向右扫描,查找第1个排序码大于temp.key的记录 */ if (i < j) data[j--] = data[i]; } data[i] = temp; /* 找到Rl的最终位置 */ quickSort(pvector, l, i-1); /* 递归处理左区间 */ quickSort(pvector, i+1, r); /* 递归处理右区间 */ } SortObject vector = {8, 49,38,65,97,76,13,27,49}; int main(){ int i; quickSort(&vector, 0, 7); for(i = 0; i < 8; i++) printf("%d ", vector.record[i]); getchar(); return 0; }
4.3.分配排序
分配排序的思想是把排序码分解成若干部分,然后通过对各个部分排序码的分别排序,最终达到整个排序码的排序。
4.3.1.基数排序
基数排序(英语:Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。
这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
#include<stdio.h> #define D 3 /* D为排序码的最大位数 */ #define R 10 /* R为基数 */ typedef int KeyType; typedef int DataType; struct Node; /* 单链表结点类型 */ typedef struct Node RadixNode; struct Node { KeyType key[D]; /* DataType info;*/ RadixNode *next; }; typedef RadixNode * RadixList; typedef struct QueueNode { RadixNode *f; /* 队列的头指针 */ RadixNode *e; /* 队列的尾指针 */ } Queue; Queue queue[R]; void radixSort(RadixList * plist, int d, int r) { int i,j,k; RadixNode *p, *head = (*plist)->next; for(j = d-1; j >= 0; j--) { /* 进行d次分配和收集*/ p = head; for(i = 0; i < r; i++) { queue[i].f = NULL; queue[i].e = NULL; /* 清队列 */ } while (p != NULL) { k = p->key[j]; /* 按排序码的第j个分量进行分配*/ if (queue[k].f == NULL) queue[k].f = p; /* 若第k个队列为空,则当前记录为队头*/ else (queue[k].e)->next = p;/* 否则当前记录链接到第k队的队尾*/ queue[k].e = p; p = p->next; } for(i = 0; queue[i].f == NULL; i++) /* 找出第一个非空队列*/ ; p = queue[i].e; head = queue[i].f; /* head为收集链表的头指针*/ for(i++; i < r; i++) if(queue[i].f != NULL) { /* 收集非空队列 */ p->next = queue[i].f; p = queue[i].e; } p->next = NULL; } (*plist)->next = head; } struct Node element[11]={ 0,0,0,NULL,/*表头*/ 0,3,6,NULL,/*36*/ 0,0,5,NULL,/*5*/ 0,1,6,NULL,/*16*/ 0,9,8,NULL,/*98*/ 0,9,5,NULL,/*95*/ 0,4,7,NULL,/*47*/ 0,3,2,NULL,/*32*/ 0,3,6,NULL,/*36*/ 0,4,8,NULL,/*48*/ 0,1,0,NULL /*10*/ }; int main(){ int i; RadixList p = element; for (i = 0; i < 10; i++) element[i].next = &element[i+1]; element[10].next = NULL; radixSort(&p, 3, 10); p = p->next; while (p != NULL){ printf("%d ", p->key[1]*10+p->key[2]); p = p->next; } getchar(); return 0; }
4.4.选择排序
选择排序的基本方法是:每步从待排序记录中选出排序码最小的记录,顺序放在已排序的记录序列的后面,直到全部排完。主要包括直接选择排序和堆排序等。
4.4.1.直接选择排序
直接选择排序的方法是:首先在所有记录中选出排序码最小的记录,与第一个记录交换,然后在其余的记录中再选出排序码最小的记录与第二个记录交换,以此类推,直到所有的记录排好序。
#include<stdio.h> #define MAXNUM 100 #define TRUE 1 #define FALSE 0 typedef int KeyType; typedef int DataType; typedef struct { KeyType key; /* 排序码字段 */ /*DataType info; 记录的其它字段 */ } RecordNode; typedef struct { int n; /* n为文件中的记录个数,n<MAXNUM */ RecordNode record[MAXNUM]; } SortObject; void selectSort(SortObject * pvector) { /* 按递增序进行直接选择排序 */ int i, j, k; RecordNode temp, *data = pvector->record; for( i = 0; i < pvector->n-1; i++ ) { /* 做n-1趟选择排序 */ k = i; for (j = i+1; j < pvector->n; j++) /* 在无序区内找出排序码最小的记录Rk*/ if (data[j].key < data[k].key) k = j; if (k != i) { /* 记录Rk与Ri互换 */ temp = data[i]; data[i] = data[k]; data[k] = temp; } } } SortObject vector={8, 49,38,65,97,76,13,27,49}; int main(){ int i; selectSort(&vector); for(i = 0; i < 8; i++) printf("%d ", vector.record[i]); getchar(); return 0; }
4.4.2.堆排序
堆排序的方法是先把待排序的记录构造成堆,然后通过从堆中不断选出最小元素,从而达到排序的目的。
#include<stdio.h> #define MAXNUM 100 #define TRUE 1 #define FALSE 0 typedef int KeyType; typedef int DataType; typedef struct { KeyType key; /* 排序码字段 */ /*DataType info; 记录的其它字段 */ } RecordNode; typedef struct { int n; /* n为文件中的记录个数,n<MAXNUM */ RecordNode record[MAXNUM]; } SortObject; /* 定义宏是为了使程序清晰 */ #define leftChild(i) (2*(i)+1) void sift(SortObject * pvector, int i, int n) { int child; RecordNode temp = pvector->record[i], *data = pvector->record; child = leftChild(i); /* Rchild是R0的左子女 */ while(child<n) { if (child < n-1 && data[child].key < data[child+1].key) child++; /* child 指向Ri的左、右子女中排序码较大的结点 */ if (temp.key < data[child].key) { data[i] = data[child]; i = child; child = leftChild(i);/* child换到父结点位置,继续调整*/ } else break; /* 调整结束 */ } data[i] = temp; /* 将记录Ri放入正确位置 */ } void heapSort(SortObject * pvector) { /* 对记录R0到Rn-1进行堆排序 */ int i, n = pvector->n; RecordNode temp, *data = pvector->record; for (i = n/2-1; i >= 0; i--) sift(pvector, i, n); /* 建立初始堆 */ for (i = n-1; i > 0; i--) { /* 进行n-1趟堆排序 */ temp = data[0]; /* 当前堆顶记录和最后一个记录互换 */ data[0] = data[i]; data[i] = temp; sift(pvector, 0, i); /* 从R0到Ri-1重建堆 */ } } SortObject vector={8, 49,38,65,97,76,13,27,49}; int main(){ int i; heapSort(&vector); for(i = 0; i < 8; i++) printf("%d ", vector.record[i]); getchar(); return 0; }
4.5.归并排序
归并排序的主要思想是:把待排序的文件分成若干个子文件,先将每个子文件内的记录排序,再将已排序的子文件合并,得到完全排序的文件。合并时开始只要比较各子文件第一个记录的排序码,排序码最小的记录为排序后的第一个记录,取出该记录,继续比较各子文件的第一个记录,找出排序后的第二个记录,如此反复,经过一次扫描,得到排序结果。
归并排序的思想可以用于内排序,但更多的用于外排序。
#include<stdio.h> #define D 3 /* D为排序码的最大位数 */ #define R 10 /* R为基数 */ typedef int KeyType; typedef int DataType; struct Node; /* 单链表结点类型 */ typedef struct Node RadixNode; struct Node { KeyType key[D]; /* DataType info;*/ RadixNode *next; }; typedef RadixNode * RadixList; typedef struct QueueNode { RadixNode *f; /* 队列的头指针 */ RadixNode *e; /* 队列的尾指针 */ } Queue; Queue queue[R]; void radixSort(RadixList * plist, int d, int r) { int i,j,k; RadixNode *p, *head = (*plist)->next; for(j = d-1; j >= 0; j--) { /* 进行d次分配和收集*/ p = head; for(i = 0; i < r; i++) { queue[i].f = NULL; queue[i].e = NULL; /* 清队列 */ } while (p != NULL) { k = p->key[j]; /* 按排序码的第j个分量进行分配*/ if (queue[k].f == NULL) queue[k].f = p; /* 若第k个队列为空,则当前记录为队头*/ else (queue[k].e)->next = p;/* 否则当前记录链接到第k队的队尾*/ queue[k].e = p; p = p->next; } for(i = 0; queue[i].f == NULL; i++) /* 找出第一个非空队列*/ ; p = queue[i].e; head = queue[i].f; /* head为收集链表的头指针*/ for(i++; i < r; i++) if(queue[i].f != NULL) { /* 收集非空队列 */ p->next = queue[i].f; p = queue[i].e; } p->next = NULL; } (*plist)->next = head; } struct Node element[11]={ 0,0,0,NULL,/*表头*/ 0,3,6,NULL,/*36*/ 0,0,5,NULL,/*5*/ 0,1,6,NULL,/*16*/ 0,9,8,NULL,/*98*/ 0,9,5,NULL,/*95*/ 0,4,7,NULL,/*47*/ 0,3,2,NULL,/*32*/ 0,3,6,NULL,/*36*/ 0,4,8,NULL,/*48*/ 0,1,0,NULL /*10*/ }; int main(){ int i; RadixList p = element; for (i = 0; i < 10; i++) element[i].next = &element[i+1]; element[10].next = NULL; radixSort(&p, 3, 10); p = p->next; while (p != NULL){ printf("%d ", p->key[1]*10+p->key[2]); p = p->next; } getchar(); return 0; }