数据结构
建立链表
写一个函数creat(),建立一个有5个学生的单向链表。
分析建立过程:
设:链表结构为: struct student { long num; float score; struct student *next; };
当输入的num等于零时,表示建立过程结束。
定义以下变量: struct student *head; /* 表头 */ struct student *p1; /* 新建结点 */ struct student *p2; /* 表尾结点 */
通过以上分析,可以得到创建链表的算法,其中,n=1表示创建的是第一个结点。
程序:
#include "stdio.h"#include "alloc.h"#include "stdlib.h"
struct student{ long num; float score; struct student *next; };
#define LEN sizeof(struct student)
/* 注意,"#define NULL 0"不需要,因为,NULL已在stdio.h中定义 */
int n;
struct student * creat() /* 创建链表,并返回表头指针 */{ struct student *head; /* 表头 */ struct student *p1; /* 新建结点 */ struct student *p2; /* 表尾结点 */
n = 0; /* 结点数为0 */ head = NULL; /* 还没有任何结点,表头为指向空 */ p1 = (struct student *)malloc(LEN); /* 创建一个新结点p1 */ p2 = p1; /* 表尾p2也指向p1 */
scanf("%ld,%f", &p1->num, &p1->score); /* 读入第一个结点的学生数据 */
while(p1->num != 0) /* 假设num=0表示输入结束 */ { n = n + 1; if (n == 1) head = p1; /* 第一个新建结点是表头 */ else p2->next = p1; /* 原表尾的下一个结点是新建结点 */
p2 = p1; /* 新建结点成为表尾 */
p1 = (struct student *)malloc(LEN); /* 新建一个结点 */ scanf("%ld,%f", &p1->num, &p1->score); /* 读入新建结点的学生数据 */ } free(p1); /* 对于num=0的结点,未加入链表,应删除其空间 */
p2->next = NULL; /* 输入结束,表尾结点的下一个结点为空 */
return (head); /* 返回表头指针 */
发表于 @ 2004年12月01日 8:27 PM | 评论 (0)
对链表的删除操作
删除一个结点的算法:
应考虑以下情况:
1、找到需要删除的结点,用p1指向它。并用p2指向p1的前一个结点。
2、要删除的结点是头结点。
3、要删除的结点不是头结点
根据以上情况分析,得到删除一个结点的算法:
程序:
在链表head中删除学号为num的结点。以表头指针head和需要删除的结点的num(学号)为参数,返回删除操作后的链表表头。
struct student * del(struct student *head, long num){ struct student *p1; /* 指向要删除的结点 */ struct student *p2; /* 指向p1的前一个结点 */
if (head == NULL) /* 空表 */ { printf("/n List is NULL/n"); return (head); } p1 = head; while(num != p1->num && p1->next != NULL) /* 查找要删除的结点 */ { p2 = p1; p1 = p1->next; } if (num == p1->num) /* 找到了 */ { if (p1 == head) /* 要删除的是头结点 */ head = p1->next; else /* 要删除的不是头结点 */ p2->next = p1->next; free(p1); /* 释放被删除结点所占的内存空间 */ /* 教材p235称:删除一个结点,并不是真正从内存把它抹掉 ?? */ printf("delete: %ld/n", num); n = n - 1; } else /* 在表中未找到要删除的结点 */ printf("%ld not found/n");
return (head); /* 返回新的表头 */}
发表于 @ 2004年11月30日 4:58 PM | 评论 (0)
单链表的简单实现(C语言)
#include "stdio.h"#include "malloc.h"#include "stdlib.h"
/* 单链表 示意图 -------------------| data | next |-------------------data : 数据域,存放数据next : 指针域,指向直接后继 */
typedef char DataType;typedef struct node { DataType data; //以字符为演示 struct node * next;}ListNode;
typedef ListNode * LinkList;
LinkList CreateListF() //头插法建链表{ printf("请输入要创建的链表数据(头插法):"); char ch; LinkList head=NULL; ListNode * s; ch=getchar(); while (ch!=’n’) { s=(ListNode *)malloc(sizeof(ListNode)); if (!s) { printf("分配内存空间失败!"); return NULL; } s->data=ch; s->next=head; head=s; ch=getchar(); } return head;}
LinkList CreateListR()//尾插法建链表{ printf("请输入要创建的链表数据(尾插法):"); char ch; LinkList head=NULL; ListNode * s=NULL, * r=NULL; while ((ch=getchar())!=’n’) { s=(ListNode *)malloc(sizeof(ListNode)); if (!s) { printf("分配内存空间失败!"); return NULL; } s->data=ch; if (head==NULL) //头为空,则说明没表,直接插入即可 head=s; else r->next=s; r=s; //直接放尾部 } if (r!=NULL) r->next=NULL; return head;}
void DispList(ListNode * p)//显示出链表的数据{ printf("n=======================n"); int ret=0; while (p!=NULL) { printf("%d -> %cn",ret,p->data); p=p->next; ret++; } printf("=======================n");}
ListNode * GetNode(LinkList head,int pos)//按位置/序号查找{ int cur_pos=0; //当前位置 ListNode * p=head; while (p->next && cur_pos<pos) { p=p->next; cur_pos++; } if (pos==cur_pos) return p; else return NULL;}
ListNode * LocateNode(ListNode * head,DataType key)//按数据查找{ ListNode * p=head->next; while (p && p->data!=key) p=p->next; return p; }
void InsertNode(LinkList head,DataType x,int i){ ListNode * p=GetNode(head,i-1); if (p==NULL) { printf("插入位置错误n"); return; } ListNode * s=(ListNode *)malloc(sizeof(ListNode)); s->data=x; s->next=p->next; // 插入新节点 p->next=s;}
void DeleteNode(LinkList head,int pos){ ListNode * p, * r; p=GetNode(head,pos-1); if (p==NULL || p->next==NULL) { printf("删除位置错误n"); return; }
r=p->next; p->next=r->next; free(r);}
//some demo void main(){ printf("=============== 链表演示程序 ================nn"); LinkList p=CreateListR(); DispList(&(*p)); ListNode * nod=GetNode(p,2); if (nod!=NULL) printf("n查找到的第2个数据为:[%c] n",nod->data);
ListNode * nod2=LocateNode(&(*p),’c’); if (nod2!=NULL) printf("n查找到的[’C’]数据为:[%c] n",nod2->data);
printf("在位置[3]处插入数据[’@’]后的链表:"); InsertNode(p,’@’,3); DispList(&(*p));
printf("删除位置[4]后的链表:"); DeleteNode(p,4); DispList(&(*p));}
发表于 @ 2004年11月30日 3:52 PM | 评论 (0)
二叉树的遍历
发布者: soarlove/* ======================================== *//* 二叉树的中序遍历 *//* ======================================== */#include <stdlib.h>
struct tree /* 树的结构宣告 */{ int data; /* 节点数据 */ struct tree *left; /* 指向左子树的指标 */ struct tree *right; /* 指向右子树的指标 */};typedef struct tree treenode; /* 树的结构新型态 */typedef treenode *btree; /* 宣告树节点指标型态 */
/* ---------------------------------------- *//* 插入二叉树的节点 *//* ---------------------------------------- */btree insertnode(btree root,int value){
btree newnode; /* 树根指标 */ btree current; /* 目前树节点指标 */ btree back; /* 父节点指标 */
/* 建立新节点记忆体 */ newnode = ( btree ) malloc(sizeof(treenode)); newnode->data = value; /* 建立节点内容 */ newnode->left = NULL; /* 设定指标初值 */ newnode->right = NULL; /* 设定指标初值 */ if ( root == NULL ) /* 是否是根节点 */ { return newnode; /* 传回新节点位置 */ } else { current = root; /* 保留目前树指标 */ while ( current != NULL ) { back = current; /* 保留父节点指标 */ if ( current->data > value ) /* 比较节点值 */ current = current->left; /* 左子树 */ else current = current->right; /* 右子树 */ } if ( back->data > value ) /* 接起父子的链结 */ back->left = newnode; /* 左子树 */ else back->right = newnode; /* 右子树 */ } return root; /* 传回树根指标 */}
/* ---------------------------------------- *//* 建立二叉树 *//* ---------------------------------------- */btree createbtree(int *data,int len){ btree root = NULL; /* 树根指标 */ int i;
for ( i = 0; i < len; i++ ) /* 用回路建立树状结构 */ root = insertnode(root,data[i]); return root;}
/* ---------------------------------------- *//* 二叉树中序遍历 *//* ---------------------------------------- */void inorder(btree ptr){ if ( ptr != NULL ) /* 终止条件 */ { inorder(ptr->left); /* 左子树 */ printf("[-]n",ptr->data); /* 列印节点内容 */ inorder(ptr->right); /* 右子树 */ }}
/* ---------------------------------------- *//* 主程式: 建立二叉树且用中序遍历列印出来. *//* ---------------------------------------- */void main(){ btree root = NULL; /* 树根指标 */
/* 二叉树节点数据 */ int data[9] = { 5, 6, 4, 8, 2, 3, 7, 1, 9 }; root = createbtree(data,9); /* 建立二叉树 */ printf("树的节点内容 n"); inorder(root); /* 中序遍历二叉树 */}
/* ======================================== *//* 二叉树的前序遍历 *//* ======================================== */#include <stdlib.h>
struct tree /* 树的结构宣告 */{ int data; /* 节点数据 */ struct tree *left; /* 指向左子树的指标 */ struct tree *right; /* 指向右子树的指标 */};typedef struct tree treenode; /* 树的结构新型态 */typedef treenode *btree; /* 宣告树节点指标型态 */
/* ---------------------------------------- *//* 插入二叉树的节点 *//* ---------------------------------------- */btree insertnode(btree root,int value){
btree newnode; /* 树根指标 */ btree current; /* 目前树节点指标 */ btree back; /* 父节点指标 */
/* 建立新节点记忆体 */ newnode = ( btree ) malloc(sizeof(treenode)); newnode->data = value; /* 建立节点内容 */ newnode->left = NULL; /* 设定指标初值 */ newnode->right = NULL; /* 设定指标初值 */ if ( root == NULL ) /* 是否是根节点 */ { return newnode; /* 传回新节点位置 */ } else { current = root; /* 保留目前树指标 */ while ( current != NULL ) { back = current; /* 保留父节点指标 */ if ( current->data > value ) /* 比较节点值 */ current = current->left; /* 左子树 */ else current = current->right; /* 右子树 */ } if ( back->data > value ) /* 接起父子的链结 */ back->left = newnode; /* 左子树 */ else back->right = newnode; /* 右子树 */ } return root; /* 传回树根指标 */}
/* ---------------------------------------- *//* 建立二叉树 *//* ---------------------------------------- */btree createbtree(int *data,int len){ btree root = NULL; /* 树根指标 */ int i;
for ( i = 0; i < len; i++ ) /* 用回路建立树状结构 */ root = insertnode(root,data[i]); return root;}
/* ---------------------------------------- *//* 二叉树前序遍历 *//* ---------------------------------------- */void preorder(btree ptr){ if ( ptr != NULL ) /* 终止条件 */ { printf("[-]n",ptr->data); /* 列印节点内容 */ preorder(ptr->left); /* 左子树 */ preorder(ptr->right); /* 右子树 */ }}
/* ---------------------------------------- *//* 主程式: 建立二叉树且用前序遍历列印出来. *//* ---------------------------------------- */void main(){ btree root = NULL; /* 树根指标 */
/* 二叉树节点数据 */ int data[9] = { 5, 6, 4, 8, 2, 3, 7, 1, 9 }; root = createbtree(data,9); /* 建立二叉树 */ printf("树的节点内容 n"); preorder(root); /* 前序遍历二叉树 */}
/* ======================================== *//* 二叉树的后序遍历 *//* ======================================== */#include <stdlib.h>
struct tree /* 树的结构宣告 */{ int data; /* 节点数据 */ struct tree *left; /* 指向左子树的指标 */ struct tree *right; /* 指向右子树的指标 */};typedef struct tree treenode; /* 树的结构新型态 */typedef treenode *btree; /* 宣告树节点指标型态 */
/* ---------------------------------------- *//* 插入二叉树的节点 *//* ---------------------------------------- */btree insertnode(btree root,int value){
btree newnode; /* 树根指标 */ btree current; /* 目前树节点指标 */ btree back; /* 父节点指标 */
/* 建立新节点记忆体 */ newnode = ( btree ) malloc(sizeof(treenode)); newnode->data = value; /* 建立节点内容 */ newnode->left = NULL; /* 设定指标初值 */ newnode->right = NULL; /* 设定指标初值 */ if ( root == NULL ) /* 是否是根节点 */ { return newnode; /* 传回新节点位置 */ } else { current = root; /* 保留目前树指标 */ while ( current != NULL ) { back = current; /* 保留父节点指标 */ if ( current->data > value ) /* 比较节点值 */ current = current->left; /* 左子树 */ else current = current->right; /* 右子树 */ } if ( back->data > value ) /* 接起父子的链结 */ back->left = newnode; /* 左子树 */ else back->right = newnode; /* 右子树 */ } return root; /* 传回树根指标 */}
/* ---------------------------------------- *//* 建立二叉树 *//* ---------------------------------------- */btree createbtree(int *data,int len){ btree root = NULL; /* 树根指标 */ int i;
for ( i = 0; i < len; i++ ) /* 用回路建立树状结构 */ root = insertnode(root,data[i]); return root;}
/* ---------------------------------------- *//* 二叉树后序遍历 *//* ---------------------------------------- */void postorder(btree ptr){ if ( ptr != NULL ) /* 终止条件 */ { postorder(ptr->left); /* 左子树 */ postorder(ptr->right); /* 右子树 */ printf("[-]n",ptr->data); /* 列印节点内容 */ }}
/* ---------------------------------------- *//* 主程式: 建立二叉树且用后序遍历列印出来. *//* ---------------------------------------- */void main(){ btree root = NULL; /* 树根指标 */
/* 二叉树节点数据 */ int data[9] = { 5, 6, 4, 8, 2, 3, 7, 1, 9 }; root = createbtree(data,9); /* 建立二叉树 */ printf("树的节点内容 n"); postorder(root); /* 后序遍历二叉树 */}
发表于 @ 2004年11月30日 3:46 PM | 评论 (0)
静态查找表及查找算法,动态查找表及查找算法和哈希表及查找算法
第一节 基本概念查找表 用于查找的数据元素集合称为查找表。查找表由同一类型的数据元素(或记录)构成。静态查找表 若只对查找表进行如下两种操作:(1)在查找表中查看某个特定的数据元素是否在查找表中,(2)检索某个特定元素的各种属性,则称这类查找表为静态查找表。静态查找表在查找过程中查找表本身不发生变化。对静态查找表进行的查找操作称为静态查找。动态查找表 若在查找过程中可以将查找表中不存在的数据元素插入,或者从查找表中删除某个数据元素,则称这类查找表为动态查找表。动态查找表在查找过程中查找表可能会发生变化。对动态查找表进行的查找操作称为动态查找。 关键字 是数据元素中的某个数据项。唯一能标识数据元素(或记录)的关键字,即每个元素的关键字值互不相同,我们称这种关键字为主关键字;若查找表中某些元素的关键字值相同,称这种关键字为次关键字。例如,银行帐户中的帐号是主关键字,而姓名是次关键字。 查找 在数据元素集合中查找满足某种条件的数据元素的过程称为查找。最简单且最常用的查找条件是"关键字值等于某个给定值",在查找表搜索关键字等于给定值的数据元素(或记录)。若表中存在这样的记录,则称查找成功,此时的查找结果应给出找到记录的全部信息或指示找到记录的存储位置;若表中不存在关键字等于给定值的记录,则称查找不成功,此时查找的结果可以给出一个空记录或空指针。若按主关键字查找,查找结果是唯一的;若按次关键字查找,结果可能是多个记录,即结果可能不唯一。查找表的存储结构 查找表是一种非常灵活的数据结构,对于不同的存储结构,其查找方法不同。为了提高查找速度,有时会采用一些特殊的存储结构。本章将介绍以线性结构、树型结构及哈希表结构为存储结构的各种查找算法。查找算法的时间效率 查找过程的主要操作是关键字的比较,所以通常以"平均比较次数"来衡量查找算法的时间效率。第二节 静态查找正如本章第一节所述:静态查找是指在静态查找表上进行的查找操作,在查找表中查找满足条件的数据元素的存储位置或各种属性。本节将讨论以线性结构表示的静态查找表及相应的查找算法。1.顺序查找1.1 顺序查找的基本思想顺序查找是一种最简单的查找方法。其基本思想是将查找表作为一个线性表,可以是顺序表,也可以是链表,依次用查找条件中给定的值与查找表中数据元素的关键字值进行比较,若某个记录的关键字值与给定值相等,则查找成功,返回该记录的存储位置,反之,若直到最后一个记录,其关键字值与给定值均不相等,则查找失败,返回查找失败标志。1.2 顺序表的顺序查找下面是顺序表的类型定义:#define MAX_NUM 100 //用于定义表的长度typedef struct elemtype{ keytype key; anytype otherelem; }Se_List[MAX_NUM],Se_Elem;假设在查找表中,数据元素个数为n(n<MAX_NUM),并分别存放在数组的下标变量a[1]~a[n]中。下面我们给出顺序查找的完整算法。int seq_search (Se_List a,keytype k){//在顺序表中查找关键字值等于k的记录,//若查找成功,返回该记录的位置下标序号,否则返回0i=1;while (i<=n && a[i].key != k) i++; if (i<=n) retrun i;else return 0;}改进算法:int seq_search2 (Se_List a,keytype k){ //设置了监视哨的顺序表查找,查找关键字值等于指定值k的记录,//若查找成功,返回记录存放位置的下标值,否则返回0 i=n ;a[0].key=k ;while ( a[i].key != k) i--; return ( i ) ;}1.3 链表的顺序查找链表的顺序查找是指将查找表作为线性表并以链式存储结构存储,用顺序查找方法查找与指定关键字值相等的记录。链表的类型定义如下所示:typedef struct linklist {keytype key; //结点的关键字类型anytype otherelem; //结点的其他成分struct linklist *next; //指向链表结点的指针}Link_Linklist,*Link;将查找表中的数据元素用这种结构的结点表示,并以指向头结点的指针标识。对链表实现顺寻查找就是在有头结点的链式查找表中查找关键字值等于给定值的记录,若查找成功,返回指向相应结点的指针,否则返回空指针。Link_Linklist *link_search (Link h , keytype k){//link为带头结点链表的头指针,查找关键字值等于k的记录,//查找成功,返回指向找到的结点的指针,查找失败返回空指针p=h->next;while ((p!=NULL) && (p->key!=k)) p=p->next; return p;}顺序查找算法简单,对表的结构无任何要求;但是执行效率较低,尤其当n较大时,不宜采用这种查找方法。2.折半查找2.1 折半查找的基本思想折半查找要求查找表用顺序存储结构存放且各数据元素按关键字有序(升序或隆序)排列,也就是说折半查找只适用于对有序顺序表进行查找。折半查找的基本思想是:首先以整个查找表作为查找范围,用查找条件中给定值k与中间位置结点的关键字比较,若相等,则查找成功,否则,根据比较结果缩小查找范围,如果k的值小于关键字的值,根据查找表的有序性可知查找的数据元素只有可能在表的前半部分,即在左半部分子表中,所以继续对左子表进行折半查找;若k的值大于中间结点的关键字值,则可以判定查找的数据元素只有可能在表的后半部分,即在右半部分子表中,所以应该继续对右子表进行折半查找。每进行一次折半查找,要么查找成功,结束查找,要么将查找范围缩小一半,如此重复,直到查找成功或查找范围缩小为空即查找失败为止。 2.2 折半查找过程示例。2.3 折半查找算法假设查找表存放在数组a的a[1]~a[n]中,且升序,查找关键字值为k。折半查找的主要步骤为: (1)置初始查找范围:low=1,high=n;(2)求查找范围中间项:mid=(low+high)/2(3)将指定的关键字值k与中间项a[mid].key比较若相等,查找成功,找到的数据元素为此时mid 指向的位置;若小于,查找范围的低端数据元素指针low不变,高端数据元素指针high更新为mid-1;若大于,查找范围的高端数据元素指针high不变,低端数据元素指针low更新为mid+1;(4)重复步骤(2)、(3)直到查找成功或查找范围空(low>high),即查找失败为止。(5)如果查找成功,返回找到元素的存放位置,即当前的中间项位置指针mid;否则返回查找失败标志。折半查找的完整算法如下:int bin_search (Se_List a, keytype k){ low=1; high=n; //置初始查找范围的低、高端指针while (low<=high){ mid=(low+high)/2; //计算中间项位置if (k==a[mid].key) break; //找到,结束循环else if (k< a[mid].key) high=mid-1; //给定值k小else low=mid+1; //给定值k大}if (low<=high) return mid ; //查找成功else return 0 ; //查找失败}第三节 动态查找1.二叉排序树二叉排序树是一种常用的动态查找表,下面首先给出它的非递归形式。二叉排序树是一棵二叉树,它或者为空,或者具有如下性质:(1)任一非终端结点若有左孩子,则该结点的关键字值大于其左孩子结点的关键字值。(2)任一非终端结点若有右孩子,则该结点的关键字值小于其右孩子结点的关键字值。二叉排序树也可以用递归的形式定义,即二叉排序树是一棵树,它或者为空,或者具有如下性质:(1)若它的左子树非空,则其左子树所有结点的关键字值均小于其根结点的关键字值。(2)若它的右子树非空,则其右子树所有结点的关键字值均大于其根结点的关键字值。(3)它的左右子树都是二叉排序树。例如,由关键字值序列(62,15,68,46,65,12,57,79,35)构成的一棵二叉排序树如图7-4所示。
如果对上述二叉排序树进行中根遍历可以得到一个关键字有序序列(12, 15, 35,46, 57,62,65,68,79),这是二叉排序树的一个重要特征,也正是由此将其称为"二叉排序树"。2.二叉排序树的查找二叉排序树的结构定义中可看到:一棵非空二叉排序树中根结点的关键字值大于其左子树上所有结点的关键字值,而小于其右子树上所有结点的关键字值,所以在二叉排序树中查找一个关键字值为k 的结点的基本思想是:用给定值k与根结点关键字值比较,如果k小于根结点的值,则要找的结点只可能在左子树中,所以继续在左子树中查找,否则将继续在右子树中查找,依此方法,查找下去,至直查找成功或查找失败为止。二叉排序树查找的过程描述如下:(1)若二叉树为空树,则查找失败,(2)将给定值k 与根结点的关键字值比较,若相等,则查找成功,(3)若根结点的关键字值小于给定值k,则在左子树中继续搜索,(4)否则,在右子树中继续查找。假定二叉排序树的链式存储结构的类型定义如下:typedef struct linklist { keytype key; anytype otherelem; struct linklist *lchild; struct linklist *rchild; }Bin_Sort_Tree_Linklist,*Bin_Sort_Tree;二叉排序树查找过程的描述是一个递归过程,若用链式存储结构存储,其查找操作的递归算法如下所示:Bin_Sort_Tree_Linklist *bt_search(Bin_Sort_Tree bt , keytype k){ //在根指针为bt的二叉排序树上查找一个关键字值为k的结点,//若查找成功返回指向该结点的指针,否则返回空指针if ( bt = NULL ) || ( bt -> key == k ) return bt; else if (k< bt -> key) return bt_search ( bt -> lchild , k ); //在左子树中搜索else return bt_search ( bt -> rchild , k ); //在右子树中搜索} 这个过程也可以用非递归算法实现,算法描述如下:Bin_Sort_Tree_Linklist *bt_search1(Bin_Sort_Tree bt , keytype k){ p = bt; //指针p指向根结点,搜索从根结点开始while ( p != NULL && p ->key != k ) { if (k <p -> key ) p = p -> lchild; else p = p -> rchild; }return ( p);}3.二叉排序树的插入在一棵二叉排序树中插入一个结点可以用一个递归的过程实现,即:若二叉排序树为空,则新结点作为二叉排序树的根结点;否则,若给定结点的关键字值小于根结点关键字值,则插入在左子树上;若给定结点的关键字值大于根结点的值,则插入在右子树上。下面是二叉排序树插入操作的递归算法。void bt_insert1(Bin_Sort_Tree *bt , Bin_Sort_Tree_Linklist *pn){ //在以bt为根的二叉排序树上插入一个由指针pn指向的新的结点if ( *bt =NULL) *bt = pn ;else if ( *bt -> key > pn->key ) bt_insert 1( &(*bt -> lchild) , pn ) ;else if ( *bt -> key < pn -> key ) bt_insert1 ( &(*bt -> rchild) , pn) ;}这个算法也可以用非递归的形式实现,如下所示:void bt_insert 2(Bin_Sort_Tree *bt , Bin_Sort_Tree_Linklist *pn){ p = bt; while ( p != NULL && p -> key!= pn->key){ q = p;if ( p ->key > pn->key ) p = p -> lchild;else p = p -> rchild;}if ( p =NULL ) { if ( q ->key > pn -> key ) q ->lchild = pn;else q -> rchild =pn ->key;}}利用二叉排序树的插入算法,可以很容易地实现创建二叉排序树的操作,其基本思想为:由一棵空二叉树开始,经过一系列的查找插入操作生成一棵二叉排序树。例如,由结点关键字序列 (62, 15, 68, 46, 65, 12 , 57 , 79, 35)构造二叉排序树的过程为:从空二叉树开始,依次将每个结点插入到二叉排序树中插入,在插入每个结点时都是从根结点开始搜索插入位置,找到插入位置后,将新结点作为叶子结点插入,经过9次的查找和插入操作,建成由这9个结点组成的二叉排序树。创建二叉排序树的算法如下:Bin_Sort_Tree_Linklist *bt_bulid (Bin_Sort_Tree a , int n){ //在数组a的a[1]~a[n]单元中存放着将要构成二叉排序树的n个结点内容bt = NULL ; for ( i =1; i <= n;i++){ p = (Bin_Sort_Tree_Linklist *)malloc (sizeof (Bin_Sort_Tree_Linklist));p ->key =a[i].key;p -> otherelem= a[i].otherelem;;p -> lchile = NULL;p -> rchile = NULL;bt_insert1( &bt , p);}return ( bt) ;}4. 二叉排序树的删除
下面分四种情况讨论一下如何确保从二叉树中删除一个结点后,不会影响二叉排序树的性质:(1)若要删除的结点为叶子结点,可以直接进行删除。(2)若要删除结点有右子树,但无左子树,可用其右子树的根结点取代要删除结点的位置。(3)若要删除结点有左子树,但无右子树,可用其左子树的根结点取代要删除结点的位置,与步骤⑵类似。(4)若要删除结点的左右子树均非空,则首先找到要删除结点的右子树中关键字值最小的结点(即子树中最左结点),利用上面的方法将该结点从右子树中删除,并用它取代要删除结点的位置,这样处理的结果一定能够保证删除结点后二叉树的性质不变。 第四节 哈希表1.哈希表的概念前面介绍的静态查找表和动态查找表的特点是:为了从查找表中找到关键字值等于某个值的记录,都要经过一系列的关键字比较,以确定待查记录的存储位置或查找失败,查找所需时间总是与比较次数有关。如果将记录的存储位置与它的关键字之间建立一个确定的关系H,使每个关键字和一个唯一的存储位置对应,在查找时,只需要根据对应关系计算出给定的关键字值k对应的值H(k),就可以得到记录的存储位置,这就是本节将要介绍的哈希表查找方法的基本思想。哈希函数 我们将记录的关键字值与记录的存储位置对应起来的关系H称为哈希函数,H(k)的结果称为哈希地址。哈希表 是根据哈希函数建立的表,其基本思想是:以记录的关键字值为自变量,根据哈希函数,计算出对应的哈希地址,并在此存储该记录的内容。当对记录进行查找时,再根据给定的关键字值,用同一个哈希函数计算出给定关键字值对应的存储地址,随后进行访问。所以哈希表即是一种存储形式,又是一种查找方法,通常将这种查找方法称为哈希查找。冲突 有时可能会出现不同的关键字值其哈希函数计算的哈希地址相同的情况,然而同一个存储位置不可能存储两个记录,我们将这种情况称为冲突,具有相同函数值的关键字值称为同义词。在实际应用中冲突是不可能完全避免的,人们通过实践总结出了多种减少冲突及解决冲突的方法。下面我们将简要地介绍一下。2. 哈希函数的构造建立哈希表,关键是构造哈希函数。其原则是尽可能地使任意一组关键字的哈希地址均匀地分布在整个地址空间中,即用任意关键字作为哈希函数的自变量其计算结果随机分布,以便减少冲突的发生可能性。常用的哈希函数的构造方法有:(1)直接定址法取关键字或关键字的某个线性函数为哈希地址。即H(key)=key 或 H(key)=a*key+b其中a,b为常数,调整 a与b的值可以使哈希地址取值范围与存储空间范围一致。(2)质数取余法取关键字被某个不大于哈希表表长n的质数m整除后所得余数为哈希地址。即H(key)=key % m (m<n,设其中n为哈希表长)。质数取余法计算简单,适用范围大,但是整数m的选择很重要,如果选择不当,会产生较多同义词,使哈希表中有较多的冲突。(3)平方取中法取关键字平方后的中间几位为哈希地址。由于平方后的中间几位数与原关键字的每一位数字都相关,只要原关键字的分布是随机的,以平方后的中间几位数作为哈希地址一定也是随机分布。(4)折叠法 把关键字折叠成位数相同的几部分,然后取这几部分的叠加作为哈希地址。在关键字位数较多,且每一位上数字的分布基本均匀时,采用折叠法,得到的哈希地址比较均匀。3. 解决冲突的方法常用的处理冲突的方法有:(1)开放定址法 当发生冲突时,在冲突位置的前后附近寻找可以存放记录的空闲单元。用此法解决冲突,要产生一个探测序列,沿着此序列去寻找可以存放记录的空闲单元。最简单的探测序列产生方法是进行线性探测,即当发生冲突时,从发生冲突的存储位置的下一个存储位置开始依次顺序探测空闲单元。 (2)链地址法 将所有关键字是同义词的记录链接成一个线性链表,将其链头链接在由哈希函数确定的哈希地址所指示的存储单元中。(3)再哈希法 当发生冲突时,用另一个哈希函数再计算另一个哈希地址,如果再发生冲突,再使用另一个哈希函数,直至不发生冲突为止。这种方法要求预先要设置一个哈希函数的序列。(4)溢出区法 除基本的存储区外(称为基本表),另外建立一个公共溢出区(称为溢出表),当发生冲突时,记录可以存入这个公共溢出区。 4. 哈希表查找及其分析哈希表的查找过程与哈希表的构造过程基本一致,对于给定的关键字值k,按照建表时设定的哈希函数求得哈希地址;若哈希地址所指位置已有记录,并且其关键字值不等于给定值k, 则根据建表时设定的冲突处理方法求得同义词的下一地址,直到求得的哈希地址所指位置为空闲或其中记录的关键字值等于给定值k为止;如果求得的哈希地址对应的内存空间为空闲,则查找失败;如果求得的哈希地址对应的内存空间中的记录关键字值等于给定值k,则查找成功。上述查找过程可以描述如下:(1)计算出给定关键字值对应的哈希地址addr=H(k);(2)while( (addr中不空)&&(addr 中关键字值!=k))按冲突处理方法求得下一地址addr;(3)如果 (addr 中为空),则查找失败,返回失败信息;(4)否则查找成功,并返回地址addr;在处理冲突方法相同的哈希表中,其平均查找时间,还依赖于哈希表的装填因子,哈希表的装填因子为:
装填因子越小,表中填入的记录就越少,发生冲突的可能性就会小,反之,表中已填入的记录越多,再填充记录时,发生冲突的可能性就越大,则查找时进行关键字的比较次数就越多。
发表于 @ 2004年11月27日 7:47 PM | 评论 (0)
排序的基本方法,内部排序主要有5种方法:插入、交换、选择、归并和基数。
内部排序与外部排序 根据在排序过程中待排序的所有数据元素是否全部被放置在内存中,可将排序方法分为内部排序和外部排序两大类。内部排序是指在排序的整个过程中,待排序的所有数据元素全部被放置在内存中;外部排序是指由于待排序的数据元素个数太多,不能同时放置在内存,而需要将一部分数据元素放置在内存,另一部分数据元素放置在外设上,整个排序过程需要在内外存之间多次交换数据才能得到排序的结果。排序算法的效率 评价排序算法的效率主要有两点:一是在数据量规模一定的条件下,算法执行所消耗的平均时间,对于排序操作,时间主要消耗在关键字之间的比较和数据元素的移动上,因此我们可以认为高效率的排序算法应该是尽可能少的比较次数和尽可能少的数据元素移动次数;二是执行算法所需要的辅助存储空间,辅助存储空间是指在数据量规模一定的条件下,除了存放待排序数据元素占用的存储空间之外,执行算法所需要的其他存储空间,理想的空间效率是算法执行期间所需要的辅助空间与待排序的数据量无关。一, 插入排序插入排序的主要思路是不断地将待排序的数值插入到有序段中,使有序段逐渐扩大,直至所有数值都进入有序段中位置。1.直接插入排序1.1 直接插入排序的基本思想 直接插入排序是一种比较简单的排序方法。它的基本思想是依次将记录序列中的每一个记录插入到有序段中,使有序段的长度不断地扩大。其具体的排序过程可以描述如下:首先将待排序记录序列中的第一个记录作为一个有序段,将记录序列中的第二个记录插入到上述有序段中形成由两个记录组成的有序段,再将记录序列中的第三个记录插入到这个有序段中,形成由三个记录组成的有序段,……依此类推,每一趟都是将一个记录插入到前面的有序段中,假设当前欲处理第i个记录,则应该将这个记录插入到由前i-1个记录组成的有序段中,从而形成一个由i个记录组成的按关键字值排列的有序序列,直到所有记录都插入到有序段中。一共需要经过n-1趟就可以将初始序列的n个记录重新排列成按关键字值大小排列的有序序列。1.3 直接插入排序算法将第i个记录插入到由前面i-1个记录构成的有序段中主要有两个步骤:⑴ 将待插入记录a[i] 保存在a[0]中,即a[0]=a[i];⑵ 搜索插入位置:j=i-1; //j最初指示i的前一个位置while (a[0].key <a[j].key) { a[j+1]=a[j]; //后移关键字值大于a[0].key的记录j=j-1; //将j指向前一个记录,为下次比较做准备}a[j+1]=a[0]; //将a[0]放置在第j+1个位置上
完整的插入排序算法为:void insertsort (DataType a, int n){ for (i=2; i<=n; i++) //需要n-1趟{ a[0]=a[i]; //将a[i]赋予监视哨j=i-1;while (a[0].key<a[j].key) //搜索插入位置{ a[j+1]=a[j]; j=j-1; }a[j+1]=a[0]; // 将原a[i]中的记录放入第j+1个位置}}直接插入排序算法简单、容易实现,只需要一个记录大小的辅助空间用于存放待插入的记录(在C语言中,我们利用了数组中的0单元)和两个int型变量。当待排序记录较少时,排序速度较快,但是,当待排序的记录数量较大时,大量的比较和移动操作将使直接插入排序算法的效率降低;然而,当待排序的数据元素基本有序时,直接插入排序过程中的移动次数大大减少,从而效率会有所提高。 插入排序是一种稳定的排序方法。2.希尔排序2.1 希尔排序的基本思想希尔排序方法又称为缩小增量排序,其基本思想是将待排序的记录划分成几组,从而减少参与直接插入排序的数据量,当经过几次分组排序后,记录的排列已经基本有序,这个时候再对所有的记录实施直接插入排序。具体步骤可以描述如下:假设待排序的记录为n个,先取整数d<n,例如,取d= n/2 ( n/2 表示不大于n/2的最大整数),将所有距离为d的记录构成一组,从而将整个待排序记录序列分割成为d个子序列,如图8-2所示,对每个分组分别进行直接插入排序,然后再缩小间隔d,例如,取d= d/2 ,重复上述的分组,再对每个分组分别进行直接插入排序,直到最后取d=1,即将所有记录放在一组进行一次直接插入排序,最终将所有记录重新排列成按关键字有序的序列。2.3 希尔排序算法(1) 分别让每个记录参与相应分组中的排序若分为d组,前d个记录就应该分别构成由一个记录组成的有序段,从d+1个记录开始,逐一将每个记录a[i]插入到相应组中的有序段中,其算法可以这样实现:for (i=d+1; i<=n; i++){将a[i]插入到相应组的有序段中;}(2) 将a[i]插入到相应组的有序段中的操作可以这样实现:l 将a[i]赋予a[0]中,即a[0]=a[i];l 让j指向a[i]所属组的有序序列中最后一个记录l 搜索a[i]的插入位置while(j>0 && a[0].key<a[j].key){ a[j+d]=a[j]; j=j-d;}希尔排序的完整算法如下:void shellsort(DataType a,int n){ for(d=n/2;d>=1;d=d/2) { for(i=1+d;i<=n;i++) //将a[i]插入到所属组的有序列段中{ a[0]=a[i]; j=i-d;while(j>0&&a[0].key<a[j].key){ a[j+d]=a[j]; j=j-d; }a[j+d]=a[0];}}在希尔排序中,由于开始将n个待排序的记录分成了d组,所以每组中的记录数目将会减少。在数据量较少时,利用直接插入排序的效率较高。随着反复分组排序,d值逐渐变小,每个分组中的待排序记录数目将会增多,但此时记录的排列顺序将更接近有序,所以利用直接插入排序不会降低排序的时间效率。希尔排序适用于待排序的记录数目较大时,在此情况下,希尔排序方法一般要比直接插入排序方法快。同直接插入排序一样,希尔排序也只需要一个记录大小的辅助空间,用于暂存当前待插入的记录。 希尔排序是一种不稳定的排序方法。二,交换排序交换排序是指在排序过程中,主要是通过待排序记录序列中元素间关键字的比较,与存储位置的交换来达到排序目的一类排序方法。1. 冒泡排序1.1 冒泡排序的基本思想冒泡排序是交换排序中一种简单的排序方法。它的基本思想是对所有相邻记录的关键字值进行比效,如果是逆顺(a[j]>a[j+1]),则将其交换,最终达到有序化。其处理过程为:(1)将整个待排序的记录序列划分成有序区和无序区,初始状态有序区为空,无序区包括所有待排序的记录。 (2)对无序区从前向后依次将相邻记录的关键字进行比较,若逆序将其交换,从而使得关键字值小的记录向上"飘浮"(左移),关键字值大的记录好像石块,向下"堕落"(右移)。每经过一趟冒泡排序,都使无序区中关键字值最大的记录进入有序区,对于由n个记录组成的记录序列,最多经过n-1趟冒泡排序,就可以将这n个记录重新按关键字顺序排列。1.3 冒泡排序算法原始的冒泡排序算法对由n个记录组成的记录序列,最多经过(n-1)趟冒泡排序,就可以使记录序列成为有序序列,第一趟定位第n个记录,此时有序区只有一个记录;第二趟定位第n-1个记录,此时有序区有两个记录;以此类推,算法框架为: for(i=n;i>1;i--){ 定位第i个记录;}若定位第i个记录,需要从前向后对无序区中的相邻记录进行关键字的比较,它可以用如下所示的语句实现。for(j=1;j< =i-1;j++)if (a[j].key>a.[j+1].key){temp=a[j];a[j]=a[j+1];a[j+1]=temp;}下面给出完成的冒泡排序算法:void BubbleSort1 (DataType a,int n){ for (i=n;i>1;i--){ for (j=1;j<=i-1;j++) if(a[j].key>a.[j+1].key){ temp=a[j];a[j]=a[j+1];a[j+1]=temp;}}}改进的冒泡排序算法在冒泡排序过程中,一旦发现某一趟没有进行交换操作,就表明此时待排序记录序列已经成为有序序列,冒泡排序再进行下去已经没有必要,应立即结束排序过程。改进的冒泡排序算法:void BubbleSort2 (DataType a,int n){for (i=n;i>1;i--){ exchange=0;for (j=1;j<=i-1;j++) if (a[j].key>a.[j+1].key){ temp=a[j];a[j]=a[j+1];a[j+1]=temp; exchange=1; }if (exchange==0) break;}}进一步地改进冒泡排序算法在【算法8-4】给出的冒泡排序算法的基础上,如果我们同时记录第i趟冒泡排序中最后一次发生交换操作的位置m(m<=n-i),就会发现从此位置以后的记录均已经有序,即无序区范围缩小在a[1]~a[m]之间,所以在进行下一趟排序操作时,就不必考虑a[m+1]~a[n]范围内的记录了,而只在a[1]~a[m]范围内进行。 完整的算法:void BubbleSort3 (DataType a,int n){ last=n-1; for (i=n;i>1;i--) { exchange=0; m=last; //初始将最后进行记录交换的位置设置成i-1for (j=1;j<=m;j++) if (a[j].key>a.[j+1].key){ temp=a[j];a[j]=a[j+1];a[j+1]=temp;exchange=1;last=j; //记录每一次发生记录交换的位置}if (exchange==0)break; }}冒泡排序比较简单,当初始序列基本有序时,冒泡排序有较高的效率,反之效率较低;其次冒泡排序只需要一个记录的辅助空间,用来作为记录交换的中间暂存单元;冒泡排序是一种稳定的排序方法。2. 快速排序 2.1 快速排序的基本思想快速排序又称为分区交换排序。其基本思想是:首先将待排序记录序列中的所有记录作为当前待排序区域,从中任选取一个记录(比如,第一个记录),并以该记录的关键字值为基准,从位于待排序记录序列左右两端开始,逐渐向中间靠拢,交替与基准记录的关键字进行比较、交换,每次比较,若遇左侧记录的关键字值大于基准记录的关键字,则将其与基准记录交换,使其移到基准记录的右侧,若遇右侧记录的关键字值小于基准值,则将其与基准记录交换,使其移至基准记录的左侧,最后让基准记录到达它的最终位置,此时,基准记录将待排序记录分成了左右两个区域,位于基准记录左侧的记录都小于或等于基准记录的关键字,位于基准记录右侧的所有记录的关键字都大于或等于基准记录的关键字,这就是一趟快速排序;然后分别对左右两个新的待排序区域,重复上述一趟快速排序的过程,其结果分别让左右两个区域中的基准记录都到达它们的最终位置,同时将待排序记录序列分成更小的待排序区域,再次重复对每个区域进行一趟快束排序,直到每个区域只有一个记录为止,此时所有的记录都到达了它的最终位置,即整个待排序记录按关键值有序排列,至此排序结束。 对待排序记录序列进行一趟快速排序的过程描述如下:(1) 初始化:取第一个记录作为基准,其关键字值为19,设置两个指针i,j分别用来指示将要与基准记录进行比较的左侧记录位置和右侧记录位置。最开始从右侧开始比较,当发生交换操作后,转去再从左侧比较;(2) 用基准记录与右侧记录进行比较:即与指针j指向的记录进行比较,如果右侧记录的关键字值大,则继续与右侧前一个记录进行比较,即j减1后,再用基准元素与j指向的记录比较,若右侧的记录小(逆序),则将基准记录与j指向的记录进行交换;(3) 用基准元素与左侧记录进行比较:即与指针i指向的记录进行比较,如果左侧记录的关键字值小,则继续与左侧后一个记录进行比较,即i加1后,再用基准记录与i指向的记录比较,若左侧的记录大(逆序),则将基准记录与i指向的记录交换,;(4) 右侧比较与左侧比较交替重复进行,直到指针i与j指向同一位置,即指向基准记录最终的位置。 一趟快速排序之后,再分别对左右两个区域进行快速排序,以此类推,直到每个分区域都只有一个记录为止。下面是对关键字值为(19,6,47,26,18,26,7,13)的记录序列进行快速排序的各趟状态2.3 快速排序算法快速排序是一个递归的过程,只要能够实现一趟快速排序的算法,就可以利用递归的方法对一趟快速排序后的左右分区域分别进行快速排序。下面是一趟快速排序的算法分析:(1) 初始化:将i 和j分别指向待排序区域的最左侧记录和最右侧记录的位置。i=first; j=end;将基准记录暂存在temp中。temp=a[i];(2) 对当前待排序区域从右侧将要进行比较的记录(j指向的记录)开始向左侧进行扫描,直到找到第一个关键字值小于基准记录关键字值的记录:while (i<j && temp.key<=a[j]) j--; (3) 如果i!=j,则将a[j]中的记录移至a[i],并将i++:a[i]=a[j]; i++;(4) 对当前待排序区域从左侧将要进行比较的记录(i指向的记录)开始向右侧进行扫描,直到找到第一个关键字值大于基准记录关键字的记录:while (i<j && a[i]<=temp.key) i++; (5) 如果i!=j,则将a[i]中的记录移至a[j],并将j++:a[j]=a[i]; j++;(6) 如果此时仍i<j,则重复上述(2)、(3)、(4)、(5)操作,否则,表明找到了基准记录的最终位置,并将基准记录移到它的最终位置上:while (i<j){ 执行(2)、(3)、(4)、(5) 步骤}a[i]=temp;下面是快速排序的完整算法。void quicksort (DataType a,int first,int end ){ i=first; j=end; temp=a[i]; //初始化while(i<j) { while (i<j && temp.key<= a[j].key) j--;a[i]=a[j]; while (i<j && a[i].key<=temp.key) i++;a[j]=a[i]; }a[i]=temp;if (first<i-1) quicksort(a, first, i-1); //对左侧分区域进行快速排序 if (i+1<end) quicksort(a, i+1, end); //对右侧分区域进行快速排序}快速排序实质上是对冒泡排序的一种改进,它的效率与冒泡排序相比有很大地提高。在冒泡排序过程中是对相邻两个记录进行关键字比较和互换的,这样每次交换记录后,只能改变一对逆序记录,而快速排序则从待排序记录的两端开始进行比较和交换,并逐渐向中间靠拢,每经过一次交换,有可能改变几对逆序记录,从而加快了排序速度。到目前为止快速排序是平均速度最大的一种排序方法,但当原始记录排列基本有序或基本逆序时,每一趟的基准记录有可能只将其余记录分成一部分,这样就降低了时间效率,所以快速排序适用于原始记录排列杂乱无章的情况。快速排序是一种不稳定的排序,在递归调用时需要占据一定的存储空间用来保存每一层递归调用时的必要信息。---------------------------------------------------------------------------------------------------三,选择排序选择排序是指在排序过程序中,依次从待排序的记录序列中选择出关键字值最小的记录、关键字值次小的记录、……,并分别将它们定位到序列左侧的第1个位置、第二个位置、……,最后剩下一个关键字值最大的记录位于序列的最后一个位置,从而使待排序的记录序列成为按关键字值由小到大排列的的有序序列。1. 简单选择排序1.1 简单选择排序的基本思想简单选择排序的基本思想是:每一趟在n-i+1(i=1,2,3,...,n-1)个记录中选取关键字最小的记录作为有序序列中的第i个记录。它的具体实现过程为:(1) 将整个记录序列划分为有序区域和无序区域,有序区域位于最左端,无序区域位于右端,初始状态有序区域为空,无序区域含有待排序的所有n个记录。(2) 设置一个整型变量index,用于记录在一趟的比较过程中,当前关键字值最小的记录位置。开始将它设定为当前无序区域的第一个位置,即假设这个位置的关键字最小,然后用它与无序区域中其他记录进行比较,若发现有比它的关键字还小的记录,就将index改为这个新的最小记录位置,随后再用a[index].key 与后面的记录进行比较,并根据比较结果,随时修改index的值,一趟结束后index中保留的就是本趟选择的关键字最小的记录位置。(3) 将index位置的记录交换到无序区域的第一个位置,使得有序区域扩展了一个记录,而无序区域减少了一个记录。不断重复 (2)、(3),直到无序区域剩下一个记录为止。此时所有的记录已经按关键字从小到大的顺序排列就位。1.3 简单选择排序算法简单选择排序的整体结构应该为:for (i=1;i<n;i){ 第i趟简单选择排序;}下面我们进一步分析一下"第i 趟简单选择排序"的算法实现。(1)初始化:假设无序区域中的第一个记录为关键字值最小的元素,即将index=i; (2)搜索无序区域中关键字值最小的记录位置:for (j=i+1;j< =n;j++)if (a[j].key<a.[index].ke) index=j;(3)将无序区域中关键字最小的记录与无序区域的第一个记录进行交换,使得有序区域由原来的i-1个记录扩展到i个记录。完整算法:void selecsort ( DataType a, int n){ for( i=1; i<n; i++) //对n个记录进行n-1趟的简单选择排序{ index=i; //初始化第i趟简单选择排序的最小记录指针for (j=i+1;j<=n;j++) //搜索关键字最小的记录位置if (a[j].key<a[i].key) index=j;if (index!=i){ temp=a[i]; a[i]=a[index]; a[index]=temp; }}}简单选择排序算法简单,但是速度较慢,并且是一种不稳定的排序方法.,但在排序过程中只需要一个用来交换记录的暂存单元。2. 堆排序2.1 堆排序的基本思想堆排序是另一种基于选择的排序方法。下面我们先介绍一下什么是堆?然后再介绍如何利用堆进行排序。堆定义:由n个元素组成的序列{k1,k2,……,kn-1,kn},当且仅当满足如下关系时,称之为堆。 ki≤k2i 或 ki≥k2i 其中i=1,2,3,... , n/2 ki≤k2i+1 kI≥k2i+1 例如序列(47,35,27,26,18,7,13,19)满足:k1 ≥ k2 k2≥ k4 k3 ≥ k6 k4 ≥ k8 k1 ≥ k3 k2 ≥ k5 k3 ≥ k7 即对任意ki (i=1,2,3,4)有: ki≥ k2iki≥ k2i+1 所以这个序列就是一个堆。若将堆看成是一棵以k1为根的完全二叉树,则这棵完全二叉树中的每个非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此可以看出,若一棵完全二叉树是堆,则根结点一定是这n个结点中的最小者或最大者。下面给出两个堆的示例。逆堆 正堆
下面我们讨论一下如何利用堆进行排序?从堆的定义可以看出,若将堆用一棵完全二叉树表示,则根结点是当前堆中所有结点的最小者(或最大者)。堆排序的基本思想是:首先将待排序的记录序列构造一个堆,此时,选出了堆中所有记录的最小者或最大者,然后将它从堆中移走,并将剩余的记录再调整成堆,这样又找出了次小(或次大)的记录,以此类推,直到堆中只有一个记录为止,每个记录出堆的顺序就是一个有序序列。2.3 堆排序算法假设当前要进行筛选的结点编号为k,堆中最后一个结点的编号为m,且a[k+1]至a[m]之间的结点都已经满足堆的条件,则调整过程可以描述为:(1) 设置两个指针i和j:i指向当前(要筛选)的结点:i=k;j指向当前结点的左孩子结点:j=2*i;(2) 比较当前结点的左右孩子的关键字值,并用j指向关键字值较大的孩子结点。if (j<m && a[j].key<a[j+1]).key ) j++;(3) 用当前结点的关键字与j所指向的结点关键字值进行比较,根据比较结果采取相应的操作,即结束筛选或交换结点内容并继续进行筛选。实现这个操作的语句为:if (a[i].key>a[j].key) break; //结束筛选操作else { temp=a[i]; a[i]=a[j]; a[j]=temp; //交换结点内容i=j;j=2*i; //准备继续筛选}可以将交换改进为:if (a[i].key>a[j].key) break; else{ a[i]=a[j]; i=j; j=2*i; }
堆排序的筛选算法:void sift (DataType a,int k,int m){ i=k;;j=2*i;temp=a[i];while (j<=m) //{if ( j < m && a[j].key < a[j+1].key ) j++;if ( a[i].key > a[j] .key) break;else{ a[i]=a[j] ;i=j;j=2*i; }}a[i] = temp;}堆排序完整的算法。void heapsort (DataType a, int n){h=n/2 ; //最后一个非终端结点的编号for ( i=h ; i>=1; i--) //初建堆。从最后一个非终端结点至根结点sift ( a, i, n ) ; for ( i=n ; i>1 ; i-- ) //重复执行移走堆顶结点及重建堆的操作{temp=a[1] ; a[1]=a[i]; a[i]=temp ;sift ( a , 1 , i-1 );}}在堆排序中,除建初堆以外,其余调整堆的过程最多需要比较树深次,因此,与简单选择排序相比时间效率提高了很多;另外,不管原始记录如何排列,堆排序的比较次数变化不大,所以说,堆排序对原始记录的排列状态并不敏感。在堆排序算法中只需要一个暂存被筛选记录内容的单元和两个简单变量h和i,所以堆排序是一种速度快且省空间的排序方法。堆排序是一种不稳定的。四,归并排序1.归并排序的基本思想归并排序是一种另一类排序方法。所谓归并是指将两个或两个以上的有序表合并成一个新的有序表。归并排序的基本思想是将一个具有n个待排序记录的序列看成是n个长度为1的有序列,然后进行两两归并,得到「n/2 个长度为2的有序序列,再进行两两归并,得到「n/4 个长度为4的有序序列,如此重复,直至得到一个长度为n的有序序列为止。3.归并排序算法通常,我们将两个有序段合并成一个有序段的过程称为2-路归并。3.1 2-路归并算法假设记录序列被存储在一维数组a中,且a[s]~a[m] 和a[m+1]~a[t] 已经分别有序,现将它们合并为一个有序段,并存入数组a1中的a1[s]~a1[t]之间。合并过程:(1) 设置三个整型变量k、i、j,用来分别指向a1[s...t]中当前应该放置新记录的位置,a[s]~a[m]和a[m+1]~a[t]中当前正在处理的记录位置。初始值应该为:i=s; j=m+1; k=s;(2) 比较两个有序段中当前记录的关键字,将关键字较小的记录放置a1[k],并修改该记录所属有序段的指针及a1中的指针k。重复执行此过程直到其中的一个有序段内容全部移至a1中为止,此时需要将另一个有序段中的所有剩余记录移至a1中。其算法实现如下:while (i<=m &&j<=t){ if (a[i].key<=a[j].key) a1[k++]=a[i++];else a1[k++]=a[j++]; }if (i<=m) while (i<=m) a1[k++]=a[i++]; else while (j<=t) a1[k++]=a[j++]; 2-路归并的完整算法:void merge (DataType a,DataType a1,int s,int m,int t){//a[s]~[m]和a[m+1]~a[t]已经分别有序,将它们归并至a1[s]~a1[t]中k=s; i=s; j=m+1;while(i<=m && j<=t){ if (a[i].key<=a[j].key) a1[k++]=a[i++]; else a1[k++]=a[j++];}if (i<=m) //将剩余记录复制到数组a1中 while ( i<=m) a1[k++]=a[i++];if (j<=t)while (j<=t) a1[k++]=a[j++];}3.2 归并排序的递归算法归并排序方法可以用递归的形式描述,即首先将待排序的记录序列分为左右两个部分,并分别将这两个部分用归并方法进行排序,然后调用2-路归并算法,再将这两个有序段合并成一个含有全部记录的有序段。递归算法:void mergesort (DataType a,DataType a1,int s,int t){ if (s==t) a1[s]=a[s]; else{ m= (s+t)/2;mergesort ( a, a2, s, m);mergesort (a, a2, m+1, t);merge (a2, a1, s, m, t);}}2-路归并排序的递归算法从程序的书写形式上看比较简单,但是在算法执行时,需要占用较多的辅助存储空间,即除了在递归调用时需要保存一些必要的信息,在归并过程中还需要与存放原始记录序列同样数量的存储空间,以便存放归并结果,但与快速排序及堆排序相比,它是一种稳定的排序方法。五, 基数排序1.基数排序的基本思想基数排序是一种基于多关键字的排序方法。1.1 多关键字排序【举例】 将表8-1所示的学生成绩单按数学成绩的等级由高到低排序,数学成绩相同的学生再按英语成绩的高低等级排序。
与前面几节所讲述的排序不同,在这个排序中,每个学生记录最终的位置由两个关键字决定。第1关键字为数学成绩k1,第二个关键字为英语成绩k2,则排序后每一个学生成绩记录的位置由关键字k1 k2决定,我们将它称之为复合关键字,即多关键字排序是按照复合关键字的大小排序。现在我们讨论一下多关键字排序的方法。下面我们以学生成绩单为例,给出通常采用的两种方法。第一种方法是先按数学等级由高到低将学生记录分成A、B、C、D、E五个子序列,然后再分别对每个子序列按英语成绩由高到低排序,这样就会得到一个优先按数学等级排序,在数学等级相同的情况下,再按英语等级排序;第二种方法是先将学生记录按英语等级由高到低分成A、B、C、D、E 五个组:
再按由高到低的顺序将它们收集起来,得到关键字序列:AA,AB,BB,BC,CB,CD,DB,EA可以看出,这个关键字序列已经是有序的了。在上述两种基于多关键字的排序方法中,第一种方法是先按高位关键字进行排序,被称之为"最高位优先"法,简称MSD法;第二种方法是先按低位关键字排序,被称之为"最低位优先"法,简称为LSD。从上面的例子可以看出:在MSD法中,先按高位关键字将待排序数据分成子序列,然后再对各子序列按下一个关键字排序;而使用LSD法进行排序时,对每个关键字都是将整个序列按关键字分组,然后按顺序收集,显然LSD法,操作比较简单。1.2 基数排序基数排序是借助于多关键字排序思想进行排序的一种排序方法。该方法将排序关键字K看作是由多个关键字组成的组合关键字,即K=k1k2…kd。每个关键字ki表示关键字的一位,其中k1为最高位,kd为最低位,d为关键字的位数。例如,对于关键字序列(101,203 567,231,478,352),可以将每个关键K看成由三个单关键字组成,即K= k1k2k3,每个关键字的取值范围为0≤ki≤9,所以每个关键字可取值的数目为10,通常将关键字取值的数目称为基数,用符号r表示,在这个例子中r=10。对于关键字序列(AB,BD,ED)可以将每个关键字看成是由二个单字母关键字组成的复合关键字,并且每个关键字的取值范围为"A~Z",所以关键字的基数r=26。我们在这里讲述的基数排序是指用多关键字的LSD方法排序,即对待排序的记录序列按照
复合关键字从低位到高位的顺序交替地进行"分组"、"收集",最终得到有序的记录序列。在此我们将一次"分组"、"收集"称为一趟。对于由 d位关键字组成的复合关键字,需要经过d趟的"分配"与"收集"。在基数排序的"分配"与"收集"操作过程中,为了避免数据元素的大量移动,通常采用链式存储结构存储待排序的记录序列,若假设记录的关键字为int类型,则链表的结点类型可以定义如下:typedef struct linklist{ int key;anytype data; int *next;}List_Linklist;3.链式基数排序算法基数排序的基本操作是按关键字位进行"分配"和"收集"。初始化操作在基数排序中,假设待排序的 记录序列是以单链表的形式给出,10个队列的存储结构也是单链表形式,其好处是:在进行"分配"操作时,按要求将每个结点插入到相应的队列中,在进行"收集"操作时,将非空的队列依次首尾相连,这样做即节省存储空间又操作方便。所以初始化操作主要是将10个队列置空:for(j=0;j<r;j++){f[j]=NULL;t[j]=NULL;}"分配"操作"分配"过程可以描述为:逐个从单链表中取出待分配的结点,并分离出关键字的相应位,然后,按照此位的数值将其插入到相应的队列中。下面我们以3位整型数值为例,说明应该如何分离出相应的关键字位? 若将3位整型数值的每一位分离出来,可以这样操作:第1次分离的关键字位(个位):k=key; 第2次分离的关键字位(十位):k=key0/10; 第3次分离的关键字位(百位):k=key00/100; ……第i次分离的关键字位:k=keyi/10i-1若假设n=10i,m=10i-1,第i次(即第i趟)分离的关键字位应利用下列表达式求出:k=key%m/n又假设n和m的初值为n=10,m=1,在每一趟分配之后,令n=n*10,m=m*10,则在第i趟"分配"时,m和n恰好为:n=10i,m=10i-1。所以第i趟"分配"操作的算法为:p=h; //p指向当前分配的结点,初始指向单链表的首结点 while(p){ k=p->key%n/m // "分离"if(f[k]==NULL) f[k]=p; //入队else t[k]->next=p; t[k]=p;p=p->next; //从单链表中获取下一个结点}m=m*10; n=n*10;"收集"操作"收集"操作实际上就是将非空队列首尾相接。具体操作可以这样实现:h=NULL; p=NULL;for(j=0;j<r;j++)if (f[j]) {if (!h) { h=f[j];p =t[j]; }else {p->next=f[j];p=t[j];}}下面是基数排序的完整算法。【算法8-12】List_Linklist *radixsort( List_Linklist *h,int d,int r){ n=10; m=1;for(i=1; i<=d;i++) //共"分配"、"收集"d次{ for(j=0;j<=9;j++) //初始化队列{ f[j]=NULL;t[j]=NULL;} p=h; while(p) { k=p->key%n/m // "分离"if(f[k]==NULL) f[k]=p; //入队else t[k]->next=p; t[k]=p;p=p->next; //从单链表中获取下一个结点}m=m*10; n=n*10;h=NULL; p=NULL; //"收集"for(j=0;j<r;j++)if (f[j]) {if (!h) { h=f[j];p =t[j]; }else {p->next=f[j];p=t[j];}}}return(h);}从基数排序的算法中可以看到:。基数排序适用于待排序的记录数目较多,但其关键字位数较少,且关键字每一位的基数相同的情况。若待排序记录的关键字有d位就需要进行d次"分配"与"收集",即共执行d趟,因此,若d值较大,基数排序的时间效率就会随之降低。基数排序是一种稳定的排序方法。
发表于 @ 2004年11月27日 7:41 PM | 评论 (0)
堆排序
1.堆的概念: 堆是一个关键字序列{k1,k2,…,kn},它具有如下特性: ki ≤k2i, ki ≤ k2i+1 或ki ≥ k2i,ki ≥ k2i+1(i=1,2,…, └n/2┘) 堆的特性在完全二叉树中解释为:完全二叉树中任一结点的关键字都小于等于(或大于等于)它的左右孩子的关键字。 如下列关键字序列均是堆: {5,23,16,68,94,72,71,73} (小顶堆) {96,83,27,38,11,9} (大顶堆) 由堆的定义可知:若关键字序列{k1,k2,…,kn}是堆,则堆顶元素是n个元素序列中的最小(或最大)元素。2.基本思想: 首先将初始待排序记录序列建堆,则堆顶元素必为含最大关键字或最小关键字的记录,输出该记录,然后将剩余记录再调整为堆,如此反复,直至排序结束。 在堆排序主要需解决两个问题: ① 如何建堆? 在完全二叉树中,所有序号i>└n/2┘的结点都是叶子,因此,以这些结点为根的子树均已是堆,这样只需将以序号为└n/2┘, └n/2┘-1, └n/2┘-2,…,1的结点为根、的子树都调整为堆即可。在按此次序调整每个结点时,其左、右子树均已是堆。 ② 若ki的左、右子树已经是堆,如何将以ki为根的完全二叉树也调整为堆? 因ki的左、右子树已经是堆,所以必须在ki 和它的左、右孩子中选出最小(或最大)的结点放到ki的位置上,不妨设k2I关键字最小,将ki与k2I交换位置,而交换之后有可能导致以k2I为根的子树不再为堆,于是可重复上述过程,将以k2I为根的子树调整为堆,……,如此逐层下去,最多可能一直调整到树叶,此方法称为"筛选法"。3.性能分析: 堆排序的时间主要由建立初始堆和不断重复建堆这两部分的时间开销构成。其中:建立初始堆时,总共需进行的关键字比较次数 ≤ 4n =O(n) ;排序过程中进行n-1次重建堆所进行的总比较次数不超过下式: 2*(└ log2(n-1) ┘+└ log2(n-2) ┘+…+ └ log22 ┘) < 2n └ log2n ┘=O(n log2n)因此堆排序总的时间复杂度是 O(n+ n log2n)= O(n log2n)。 堆排序在最坏情况下的时间复杂度也是O(nlog2n),相对于快速排序来说,这是堆排序的最大优点。 另外,堆排序仅需一个记录大小供交换用的辅助空间。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录较少的文件,但对n较大的文件还是很有效的。
堆排序的程序实现
//* * * * * * * * * * * * * * * * * * * * * * * *//*PROGRAM :堆排序 *//*CONTENT :堆排序 *//* * * * * * * * * * * * * * * * * * * * * * * *#include <dos.h>#include <conio.h>#include <math.h>#include <stdio.h>#include <stdlib.h>#define MAXSIZE 20 //排序表的最大容量typedef struct //定义排序表的结构{int elemword[MAXSIZE]; //数据元素关键字int length; //表中当前元素的个数}SqList;void InitialSqList(SqList&); //初始化排序表void HeapSort(SqList &); //堆排序void HeapAdjust(SqList &,int,int); //堆调整void PrintSqList(SqList); //显示表中的所有元素void main(){SqList L; //声明表Lchar j='y';
//-------------------------程序说明-------------------------------printf("本程序将演示堆排序的操作。/n");//----------------------------------------------------------------while(j!='n'&&j!='N'){InitialSqList(L); //待排序列初始化HeapSort(L); //堆排序PrintSqList(L); //显示排序结果printf("继续进行下一次排序吗?(Y/N)");scanf(" %c",&j);}printf("程序运行结束!/n按任意键关闭窗口!/n");getchar();getchar();}void InitialSqList(SqList &L){//表初始化int i;printf("请输入待排序的记录的个数:");scanf("%d",&L.length);printf("请输入待排序的记录的关键字(整型数):/n");for(i=1;i<=L.length;i++)scanf("%d",&L.elemword[i]);}void HeapSort(SqList &L){//对顺序表L做堆排序。int i,j,t;for(i=L.length/2;i>0;--i) //把L.elemword[1..L.length]建成大顶堆HeapAdjust(L,i,L.length);for(i=L.length;i>1;--i){t=L.elemword[1]; //将堆顶记录和当前未经排序子序列L.elemword[1..i] L.elemword[1]=L.elemword[i]; //中的最后一个记录相互交换L.elemword[i]=t;HeapAdjust(L,1,i-1); //将L.r[1..i-1]重新调整为大顶堆}}void HeapAdjust(SqList &H,int s,int m){//已知H.elemword[s..m]中除H.elemword[s]之外均满足堆的定义,本函数调整H.elemword[s]//使H.elemword[s..m]成为一个大顶堆int j,rc;rc=H.elemword[s];for(j=2*s;j<=m;j*=2) //沿关键字叫大的结点向下筛选{if(j<m&&H.elemword[j]<H.elemword[j+1]) ++j; //j为关键字较大的记录的下标if(rc>=H.elemword[j]) break; //rc应插入在位置s上H.elemword[s]=H.elemword[j];s=j;}H.elemword[s]=rc; //插入}void PrintSqList(SqList L){//显示表中所有元素int i;printf("已排好序的序列如下:/n");for(i=1;i<=L.length;i++)printf("M",L.elemword[i]);printf("/n");}
发表于 @ 2004年11月07日 12:02 PM | 评论 (0)
冒泡排序
冒泡排序
1. 基本思想: 比较相邻两个记录的关键字,若r[i].key>r[i+1].key,则交换之,其中i 从0到n-pass-1(pass的初值为1)称之为一趟起泡排序, 其结果是使最大关键字的记录被交换到n-pass的位置上。 如果某一趟起泡排序过程中没有进行一次记录的交换,则排序过程结束。最坏情况下需n-1趟排序。 2. 性能分析: 若记录序列的初始状态为"正序",则冒泡排序过程只需进行一趟排序,在排序过程中只需进行n-1次比较,且不移动记录;反之,若记录序列的初始状态为"逆序",则需进行n(n-1)/2次比较和记录移动。因此冒泡排序总的时间复杂度为O(n*n)。
3.冒泡排序的程序实现:
//* * * * * * * * * * * * * * * * * * * * * * * *//*PROGRAM :起泡排序 *//*CONTENT :起泡排序 *//* * * * * * * * * * * * * * * * * * * * * * * *#include <dos.h>#include <conio.h>#include <math.h>#include <stdio.h>#include <stdlib.h>#define MAXSIZE 20 //排序表的最大容量enum BOOL{False,True};typedef struct //定义排序表的结构{int elemword[MAXSIZE]; //数据元素关键字int count; //表中当前元素的个数}SqList;void InitialSqList(SqList&); //初始化排序表void BubbleSort(SqList &); //起泡排序void PrintSqList(SqList); //显示表中的所有元素void main(){SqList L; //声明表Lchar j='y';//-------------------------程序说明-------------------------------printf("本程序将演示起泡排序的操作。/n");//----------------------------------------------------------------while(j!='n'&&j!='N'){InitialSqList(L); //待排序列初始化BubbleSort(L); //起泡排序PrintSqList(L); //显示排序结果printf("继续进行下一次排序吗?(Y/N)");scanf(" %c",&j);}printf("程序运行结束!/n按任意键关闭窗口!/n");getchar();getchar();}void InitialSqList(SqList &L){//表初始化int i;printf("请输入待排序的记录的个数:");scanf("%d",&L.count);printf("请输入待排序的记录的关键字(整型数):/n");for(i=0;i<L.count;i++)scanf("%d",&L.elemword[i]);}void BubbleSort(SqList &L){//对顺序表L做起泡排序。int i,j,t;BOOL change;for(i=L.count-1,change=True;i>0&&change;--i){change=False;for(j=0;j<i;j++)if(L.elemword[j]>L.elemword[j+1]){t=L.elemword[j];L.elemword[j]=L.elemword[j+1];L.elemword[j+1]=t;change=True;}}}void PrintSqList(SqList L){//显示表中所有元素 int i;printf("已排好序的序列如下:/n");for(i=0;i<L.count;i++)printf("M",L.elemword[i]);printf("/n");}