读完每个章节整理一次笔记。
第1章 数据结构绪论
程序设计 = 数据结构 + 算法
数据结构是相互之间存在一种或多种特定关系的数据元素的集合
逻辑结构
- 集合结构:相互无关系。
- 线性结构:一对一的关系。
- 树形结构:一对多的关系。
- 图形结构:多对多的关系。
物理结构
- 顺序存储结构:数据元素存放在地址连续的存储单元。
- 链式存储结构:数据存放在任意的存储单元,可以是连续的,也可以是不连续的。数据元素之间通过指针关联。
数据类型是指一组性质相同的数据元素的集合以及定义在此集合上的一些操作的总称。
抽象数据类型是指一个数学模型以及定义在该模型上的一组操作。
第2章 算法
算法的定义:算法是解决特定问题求解步骤的描述,在计算机中为指令的有限序列,并且每条指令表示一个或多个操作。
算法的特性:有穷性、确定性、可行性、输入、输出。
算法的设计要求:正确性、可读性、健壮性、高效率和低存储量需求。
一个高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
- 算法采用的策略
- 编译产生的代码质量(由软件支持)
- 问题的输入规模
- 机器执行指令的速度(由硬件决定)
第3章 线性表
线性表List是零个或多个数据元素的有限序列。
线性表的顺序存储结构
顺序存储结构指的是用一段地址连续的存储单元依次存储线性表的数据元素。
顺序存储结构的三个属性:
- 存储空间的起始位置
- 线性表的最大容量
- 线性表的当前长度
顺序存储结构的优点:
- 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存取标重的任一位置的元素,时间复杂度为O(1)
顺序存储结构的缺点:
- 插入和删除操作需要移动大量元素,时间复杂度为O(n)
- 当线性表长度变化较大时,难以确定存储空间的容量
- 造成存储空间的碎片
线性表的链式存储结构
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的或不连续的。
结点由数据域和指针域组成。
单链表是结点中指包含一个指针域的链表。
链表的第一个结点的存储位置叫头指针。头指针是链表的必要元素。
链表的第一个结点前附设的一个结点叫头结点。头结点不是链表的必要元素,但头结点的存在可以让第一个结点的插入删除操作与其他结点保持一致。
单链表的读取操作的时间复杂度是O(n),插入和删除操作的时间复杂度是O(1)。
若线性表需要很频繁的查找且很少进行插入和删除的操作,宜采用顺序结构。若线性表的大小是未知的,应当采用链式结构。
静态链表
静态链表由数组实现,其结点由两个数据域组成,其中一个数据域表示下一个结点的下标,另一个数据域表示该结点的值。数组的第一个下标对应的结点用来表示下一个即将被使用的空闲空间的下标;数组的最后一个结点用来表示链表的第一个结点的下标。
静态链表的优点
- 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了顺序存储结构中的缺点(插入和删除的操作需要移动大量的元素)。
静态链表的缺点
- 没有解决连续存储分配带来的表长难以确定的问题
- 失去了顺序存储结构随机存取的特性
public class Link_list
{
public int Length{get;set;}
static readonly int MAXSIZE = 10;
public static Link[] _arry;
public Link_list()
{
Length = 0;
_arry = new Link[MAXSIZE];
Init();
}
/// <summary>
/// 初始化静态链表
/// </summary>
private void Init()
{
for (int i = 0; i < MAXSIZE; i++)
{
_arry[i] = new Link();
_arry[i].cur = i + 1;
}
_arry[MAXSIZE - 1].cur = 1;
}
/// <summary>
/// 返回第一个可用的位置
/// </summary>
/// <returns></returns>
private int Mallo_()
{
int i = _arry[0].cur;
if (_arry[0].cur > 0)
{
_arry[0].cur = _arry[i].cur;
}
return i;
}
/// <summary>
/// 添加一个数据项
/// </summary>
/// <param name="l"></param>
public void Add(Link l)
{
int temp = Mallo_();
_arry[temp].data = l.data;
_arry[temp].cur = _arry[0].cur;
addLength();
}
private void addLength()
{
Length++;
}
private void delteLength()
{
Length--;
}
/// <summary>
/// 在指定位置前插入数据
/// </summary>
/// <param name="weiz">指定位置</param>
/// <param name="l">要插入的数据</param>
public void Insert(int weiz, Link l)
{
if (weiz > _arry[0].cur)
{
return;
}
int free_Length = Mallo_();
int temp = MAXSIZE - 1;
for (int i = 1; i <= weiz - 1; i++)
{
temp = _arry[temp].cur;
}
l.cur = _arry[temp].cur;
_arry[temp].cur = free_Length;
_arry[free_Length] = l;
addLength();
}
public void Delete(int weiz)
{
if (weiz >= _arry[0].cur)
{
return;
}
int k = _arry[MAXSIZE - 1].cur;
for (int i = 1; i < weiz - 1; i++)
{
k = _arry[k].cur;
}
int tmep = _arry[k].cur;
_arry[k].cur = _arry[tmep].cur;
_free(tmep);
delteLength();
}
private void _free(int i)
{
_arry[i].cur = _arry[0].cur;
_arry[i].data = null;
_arry[0].cur = i;
}
}
public class Link
{
public string data;
public int cur;
}
循环链表
单链表中的尾结点的指针域由空指针改为指向头结点,使得整个单链表头尾相连,形成一个环。
双向链表
单链表中的每个节点中有两个指针域,一个指向前驱结点,一个指向后驱结点。
第4章 栈与队列
栈
栈限定在表尾进行插入和删除操作的线性表,允许插入和删除的一端称为栈顶Top,另一端称为栈底Bottom,不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
栈的顺序存储结构
- 顺序栈由顺序线性表和一个指向栈顶的指针组成。
- 需要实现确定一个长度,可能会造成内存空间的浪费,但是存取时效率很高。
- 存取时间复杂度为O(1)
栈的链式存储结构
- 链栈由链式线性表和一个指向栈顶的指针组成。
- 因为要求了每个数据元素具备指针域,所以造成了一定量的内容开销,但是链栈的长度是没有限制的。
- 存取时间复杂度为O(1)
栈的应用
用递归实现斐波那契数列(前面相邻两项之和构成第三项)
int Fbi(int i)
{
if(i<2) return i==0?0:1;
return Fbi(i-1)+Fbi(i-2);
}
递归的定义:
- 把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。
- 每个递归至少有一个条件满足时递归不再进行,即不在引用自身而是返回值。
大量的递归调用会建立函数的副本,消费大量的时间和内存。
队列
队列只允许在一端进行插入操作而在另一端进行删除操作的线性表,允许插入的一端称为队尾,允许删除的一端称为队头。队列是一种先进先出(First In First Out)的线性表,简称FIFO。
循环队列的定义:头尾相连的顺序存储结构称为循环队列。
通用的计算队列长度的公式:(rear-front+QueueSize)%QueueSize
第5章 串
串是由零个或多个字符组成的有限序列,又名叫字符串。
零个字符的字符串称为空串。
子串在主串中的位置就是子串第一个字符在主串中的序号。
串的比较是通过组成串的字符之间的编码来进行的,而字符的编码指的是字符在对应的字符集中的编号。常见的如ASCII编码用8位二进制表示一个字符,可以表示256个字符;Unicode编码用16位二进制表示一个字符,其前256个字符与ASCII码相同。
串的顺序存储结构
- 用一组地址连续的存储单元来存储串中的字符序列。一般可以将实际的串长度保存在数组的0下标位置,也可以在串的值后面加一个不计入串长度的结束标记符号,比如\0来表示串值的结束。
串的链式存储结构
- 一个结点可以存放一个字符,也可以考虑存放多个字符,最后一个结点若是未占满时,可以使用非串值字符来补全。
链式存储结构在连接串与串操作时有一定优势,但是总体不如顺序存储结构灵活,性能也不如顺序存储结构。
朴素模式匹配算法(暴力解法)
- 子串的定位操作通常称作串的模式匹配。
- 对主串做大循环,对子串做小循环,直到匹配或者遍历完成为止。
KMP模式匹配算法
- 对于在子串中有与首字符相等的字符,可以省略一些不必要的判断步骤。
//KMP算法还不是很理解,挖一个坑。
第6章 树
树Tree是n(n>=0)个结点的有限集。n=0时,称为空树。在任意一颗非空树中:有且仅有一个根Root的结点;当n>1时,其余结点可分为m(m>0)个互不相交的有限集,其中每个集本身又是一棵树,并且称为根的子树。
结点拥有的子树的个数称为结点的度。度为0的结点称为叶结点或终端结点;度不为0的结点称为非终端结点或分支结点。树的度是树内各结点的度的最大值。
树中结点的最大层次称为树的深度或高度。
如果将树中结点的各子树堪称从左到右是有次序的,不能交换的,则称该树为有序树;否则称为无序树。
二叉树
- 每个结点最多有两颗子树;
- 左右树是有序的,不能颠倒;
特殊二叉树
- 斜树:所有结点都只有左(右)子树的二叉树叫左(右)斜树。
- 满二叉树:所有分支结点都存在左子树和右子树;并且所有叶子都在同一层上。
- 完全二叉树:满二叉树是一个完全二叉树。完全二叉树是一个除了叶子结点所在层外,其他层满足满二叉树的要求的二叉树。叶子结点所在层是集中在左边连续的。
二叉树的性质
- 在二叉树的第i层上至多有2^(i-1)个结点。
- 深度为k的二叉树至多有2^k-1个结点(k>=1)。
- 对任何一颗二叉树T,如果其终端结点数目为n0,度为2的节点数为n2,则n0=n2+1。
- 具有n个结点的完全二叉树大的深度为|log2(n)|+1。
- 如果对一颗树有n个结点的完全二叉树的结点按层序号编号,对任一结点i(1 <= i <= n),有:
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则双亲是结点的|i/2|
- 如果2i>n,则结点i无左孩子,否则左孩子是2i。
- 如果2i+1>n,则结点i无右孩子,否则右孩子是结点2i+1。
二叉树的顺序存储结构
- 顺序存储结构一般只用于完全二叉树,数组下标体现结点之间的逻辑关系。
二叉树的链式存储结构
- 每个结点有数据域和两个指针域。
遍历二叉树
- 前序遍历:根左右
- 中序遍历:左根右
- 后序遍历:左右根
- 层序遍历:从左到右、从上到下逐层遍历
前序遍历算法
void PreOrderTraverse(Tree T)
{
if(T==null)
return;
print(T.Data);
PreOrderTraverse(T.LeftChild);
PreOrderTraverse(T.RigthChild);
}
推导遍历结果
- 已知前序遍历序列和中序遍历序列,可以唯一确定一颗二叉树。
- 已知后序遍历序列和中序遍历序列,可以唯一确定一颗二叉树。
- 已知前序遍历序列和后序遍历序列,不可以唯一确定一颗二叉树。
线索二叉树
指向前驱和后续的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。
对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化。
搜索二叉树的存储结构定义
- 当ltag=0时,lchild指向前驱结点;
- 当ltag=1时,lchild指向左结点;
- 当rtag=0时,rchild指向后继结点;
- 当rtag=1时,rchild指向右结点;
struct TreeNode<T>
{
T data;
TreeNode lchild;
TreeNode rchild;
int ltag;//左标识
int rtag;//右标识
}
搜索二叉树的好处是我们可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。适用于二叉树经常需要按某种遍历顺序来遍历或查找的情况。
树、森林均可以转换成二叉树来处理。
赫夫曼树
一颗有n个叶子结点的二叉树,每个叶子结点的权值记为{w1,w2...wn},每个叶子结点的路径长度为{l1,l2...ln},带权路径长度(WPL=w1*l1+w2*l2+...+wn*ln)最小的二叉树,称为赫夫曼树。
应用:赫夫曼编码
- 规定赫夫曼树的左分支代表0,右分支代表1,结点的序列根据从根结点到该结点所走过的路径来决定。赫夫曼编码在保证数据不出错的情况下,压缩了数据的长度,节省了空间。
第7掌 图
图是由顶点的有穷非空集合和顶点之间边的集合组成。
图按照有无方向分为无向图和有向图。无向图由顶点和边构成,有向图有顶点和弧组成。弧有弧尾和弧头之分。
图按照边或弧的多少分为稀疏图和稠密图。
如果任意两顶点之间都存在边叫完全图,有向的叫有向完全图。
若无重复的边或顶点到自身的图则叫简单图。
无向图顶点的边数叫做度,有向图顶点分为入度和出度。
图上的边或弧带权则称为网。
图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复叫简单路径。
若任意两顶点都是连通的,则图就是连通图,有向则称强连通图。
若图中有子图,子图极大连通则是连通分量,有向的则称强连通分量。
无向图中连通且n个顶点有n-1条边则叫生成树。
有向图中一顶点入度为0,其余顶点入度为1的叫有向树。
存储结构
图的邻接矩阵是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组存储图中边或弧的信息。
图的邻接表是用数组和链表相结合来表示图。一个一维数组存储图中顶点信息,一个链表存储顶点的关系。
有向图的十字链表
- 把邻接表和逆邻接表结合在一起。
- 使得寻找以某顶点为尾的弧或者以某顶点为头的弧更加容易。
- 计算顶点的出度和入度更加容易。
struct Node
{
data;
firstin;//入边表头指针,指向该顶点的入边表中的第一个结点。
firstout;//出边表头指针,指向该顶点的出边表中的第一个结点。
}
struct Line
{
tailvex;//弧起点在顶点表的下标
headvex;//弧终点在顶点表中的下标
headlink;//入边表指针域,指向终点相同的下一条边
taillink;//出边表指针域,指向起点相同的下一条边
weight;//如果是网,可以存权值
}
邻接多重表
struct node
{
//ivex jvex是与某条边依附的两个顶点在顶点表中的下标
ivex;
jvex;
//ilink 指向依附顶点ivex的下一条边
ilink;
//jlink 指向依附顶点jvex的下一条边
jlink;
}
边集数组
边集数组由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。
图的遍历
深度优先算法:用递归实现
广度优先算法:用队列实现
深度优先更适合目标比较明确,以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。
最小生成树
- 迪杰斯特拉算法
- 佛洛依德算法
拓扑排序
- 拓扑排序介绍
- 拓扑排序算法
关键路径
- 关键路径算法原理
- 关键路径算法
第8章 查找
顺序表查找
//顺序表查找
public static int Sequential_Search1(int[] array, int key)
{
for (int i = 0; i < array.Length; i++)
{
if (array[i] == key)
{
return i;
}
}
return -1;
}
//顺序表查找优化
public static int Sequential_Search2(int[] array, int key)
{
int i = array.Length;
array[0] = key;//数组0下标存关键数字
while (array[i] != key)
{
i--;
}
return i;
}
有序表的二分查找
//有序表的二分查找
public static int Binary_Search(int[] array, int key)
{
int low = 0, high = array.Length - 1, mid = 0;
while (low <= high)
{
mid = (low + high) / 2;
if (array[mid] == key)
{
return mid;
}
else if (array[mid] > key)
{
low = mid + 1;
}
else
{
high = high - 1;
}
}
return 0;
}
有序表的插值查找
//有序表的插值查找法
//mid = low + (high-low)*(key-a[low])/(a[high]-a[low])
//适用于数据比较均衡的有序表
有序表的斐波那契查找
//斐波那契查找
public static int Fibonacci_Search(int[] array, int key)
{
Console.WriteLine("斐波那契数列");
int[] F = GetFibonacciArray(array.Length);
for (int i = 0; i < F.Length; i++)
{
Console.Write(F[i] + " ");
}
int k = 0;
while (array.Length > F[k] - 1)
{
k++;
}
Console.WriteLine("\nK = " + k);
int[] temp = new int[F[k] - 1];
for(int i = 0; i < array.Length; i++)
{
temp[i] = array[i];
Console.Write(temp[i] + " ");
}
for (int i = array.Length; i < F[k] - 1; i++)
{
temp[i] = array[array.Length - 1];
Console.Write(temp[i] + " ");
}
int low = 0, high = array.Length - 1, mid = 0;
k -= 1;
while (low <= high)
{
mid = low + F[k] - 1;
if (temp[mid] > key)
{
high = mid - 1;
k = k - 1;
}
else if (temp[mid] < key)
{
low = mid + 1;
k = k - 2;
}
else
{
if (mid < array.Length)
{
return mid;
}
else
{
return array.Length-1;
}
}
}
return 0;
}
//获取一个长度为n的斐波那契数列
static int[] GetFibonacciArray(int n)
{
int[] array = new int[n];
if (n >= 1)
{
array[0] = 0;
}
if (n >= 2)
{
array[1] = 1;
}
for (int i = 2; i < n; i++)
{
array[i] = array[i - 1] + array[i - 2];
}
return array;
}
线性索引查找
稠密索引
- 数据集中每一个记录对应一个索引项
分块索引
- 数据集分为若干块,且满足块内无序和块间有序。
倒排索引
- 不是由记录来确定属性值,而是由属性值来确定记录的位置。
二叉排序树创建
public class BitNode
{
public int data;
public BitNode lchild;
public BitNode rchild;
}
public class BST
{
public static void SearchBST(ref BitNode T, int key, BitNode new_child)
{
if (T == null)
{
T = new_child;
}
else if (key == T.data)
{
return;
}
else if (key > T.data)
{
SearchBST(ref T.rchild, key, new_child);
}
else
{
SearchBST(ref T.lchild, key, new_child);
}
}
public static void InsertBST(ref BitNode T, int key)
{
BitNode s = new BitNode();
s.data = key;
if (T == null)
{
T = s;
}
else
{
SearchBST(ref T, key, s);
}
}
public static void OutPut(BitNode T)
{
if (T == null)
{
return;
}
OutPut(T.lchild);
Console.WriteLine(T.data);
OutPut(T.rchild);
}
public static void DeleteBST(ref BitNode T, int key)
{
if (T == null)
{
return;
}
else
{
if (T.data == key)
{
Delete(ref T);
}
else if (T.data > key)
{
DeleteBST(ref T.lchild, key);
}
else
{
DeleteBST(ref T.rchild, key);
}
}
}
private static void Delete(ref BitNode p)
{
if (p.rchild == null)
{
BitNode q;//待删除的节点
q = p;
p = p.lchild;
q = null;
}
else if (p.lchild == null)
{
BitNode q;//待删除的节点
q = p;
p = p.rchild;
q = null;
}
else
{
BitNode q;//待删除的节点的父节点
BitNode s;//待删除的节点
q = p;
s = p.lchild;
while (s.rchild != null)
{
q = s;
s = s.rchild;
}
p.data = s.data;
if (q != p)
{
q.rchild = s.lchild;
}
else
{
q.lchild = s.lchild;
}
s = null;
}
}
}
----------------------------------------------------------------
static void Main(string[] args)
{
int[] a = { 6, 5, 3, 4, 2, 7, 1, 9, 8, 0 };
BitNode T = null;
for(int i = 0; i < 10; i++)
{
BST.InsertBST(ref T,a[i]);
}
BST.InsertBST(ref T,6);
BST.OutPut(T);
Console.Read();
}
平衡二叉树(AVL树)
解决二叉排序树出现的不平衡状态,如左/右斜树。
平衡二叉树的每一个节点的左子树和右子树的高度差至多等于1。
将二叉树节点上的左子树深度减去右子树深度的值称为平衡因子BF,该值只可能是-1、0、1。
核心思想就是把不平衡消灭在最早时刻。
查找、插入、删除的时间复杂度均为O(logn)。
多路查找树
每一个结点的孩子可以多于两个,每个节点可以存储多个元素。
- 2-3树:每一个结点都是2结点或3结点。2结点:包含一个元素和两个孩子。3结点:包含两个元素和三个孩子。元素的大小,孩子的元素的大小都是有序的。
- 2-3-4树:每一个结点都是2结点或3结点或4结点。4结点:包含三个元素和四个孩子。
- B树:平衡的多路查找树。结点最大的孩子数目称为B树的阶。所有叶子节点都位于同一层次。
- B+树:在B树结构基础上加上了新的元素组织方式。叶子节点里包含关键字的信息。
- B+树的好处:如果要随机查找可从根节点出发;如果要顺序查找可从叶子结点出发。
散列表
散列技术:在记录的存储位置和他的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。f称为散列函数或哈希函数。
散列表(哈希表):采用散列技术将记录存储在一块连续的存储空间中。关键字对应的记录存储位置我们称为散列地址。
散列冲突:两个不相同的关键字key1和key2,却有f(key1)=f(key2)。
散列函数的构造原则:
- 计算简单
- 散列地址分布均匀
散列函数的构造方法:
- 直接定址法
- 取关键字的某个线性函数值为散列地址
- 简单、均匀、没有冲突。需要实现知道关键字的分布情况,仅适合查找表较小且连续的情况。
- 数字分析法
- 抽取关键字的一部分作为散列表的关键字
- 适合处理关键字位数较大的情况。
- 平方取中法
- 适用于不知道关键字的分布,而位数又不是很大的情况。
- 折叠法
- 将关键字从左到右分割成位数相等的几部分,然后将这几部分叠加求和。
- 不需要知道关键字的分布,适合关键字位数较多的情况。
- 除留余数法
- 对于散列表长为m的散列函数公式为: f(key) = key mod p (p<=m)
- p最好为不大于m的最大质数
- 随机数法
- f(key) = random(key)
视不同情况选择不同的散列函数:
- 计算散列地址所需的时间
- 关键字长度
- 散列表大小
- 关键字分布情况
- 记录查找的频率
第9章 排序
排序的稳定性:对于关键值大小相同的两个数在排序前后,前后顺序仍然不变,则是稳定的。
内排序和外排序:排序过程中,是否将所有的数据放到内存中。
高效率的内排序算法应该是具有尽可能少的关键字比较次数和尽可能少的记录移动次数。
内排序:插入排序、交换排序、选择排序、归并排序。
冒泡排序
public void BubbleSort(ref int[] array)
{
bool flag = true;
for (int i = 1; i < array.Length && flag; i++)
{
flag = false;
for (int j = array.Length - 1; j >= i; j--)
{
if (array[j - 1] > array[j])
{
swap(ref array, j - 1, j);
flag = true;
}
}
}
}
选择排序
public void SelectSort(ref int[] array)
{
for (int i = 0; i < array.Length; i++)
{
int min = i;
for (int j = i + 1; j < array.Length; j++)
{
if (array[min] > array[j])
{
min = j;
}
}
if (min != i)
{
swap(ref array, i, min);
}
}
}
插入排序
public void InsertSort(ref int[] array)
{
int i, j, temp;
for (i = 1; i < array.Length; i++)
{
if (array[i] < array[i - 1])
{
temp = array[i];
for (j = i - 1; j >= 0 && array[j] > temp; j -= 1)
{
array[j + 1] = array[j];
}
array[j + 1] = temp;
}
}
}
希尔排序
增量序列的最后一个增量值必须等于1才行。
public void ShellSort(ref int[] array)
{
int i, j, temp;
int increment = array.Length;
do
{
increment = increment / 3 + 1;
for (i = increment; i < array.Length; i++)
{
if (array[i] < array[i - increment])
{
temp = array[i];
for (j = i - increment; j >= 0 && temp < array[j]; j -= increment)
{
array[j + increment] = array[j];
}
array[j + increment] = temp;
}
}
} while (increment > 1);
}
堆排序
大顶堆:每个结点的值都大于或等于其左右孩子结点的值。
小顶堆:每个结点的值都小于或等于其左右孩子结点的值。
简单总结下堆排序的基本思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
public void HeapSort(ref int[] array)
{
int i;
for (i = (array.Length - 1) / 2; i >= 0; i--)
{
HeapAdjust(ref array, i, array.Length - 1);
}
for (i = array.Length-1; i > 0; i--)
{
swap(ref array, 0, i);
HeapAdjust(ref array, 0, i - 1);
}
}
void HeapAdjust(ref int[] array, int s, int m)
{
int temp, j;
temp = array[s];
for (j = 2 * s; j <= m; j *= 2)
{
if (j < m && array[j] < array[j + 1])
{
++j;
}
if (temp >= array[j])
{
break;
}
array[s] = array[j];
s = j;
}
array[s] = temp;
}
归并排序
public void MergeSort(ref int[] array)
{
MSort(ref array, 0, array.Length - 1);
}
private void MSort(ref int[] array, int first, int end)
{
if (first < end)
{
int mid = (first + end) / 2;
MSort(ref array, first, mid);
MSort(ref array, mid + 1, end);
Merge(ref array, first, mid, end);
}
}
private void Merge(ref int[] array, int first, int mid, int end)
{
int[] temp = new int[end - first + 1];
int i, j, k = 0;
for (i = first, j = mid + 1; i <= mid && j <= end;)
{
if (array[i] < array[j])
{
temp[k++] = array[i++];
}
else
{
temp[k++] = array[j++];
}
}
while (i <= mid)
{
temp[k++] = array[i++];
}
while (j <= end)
{
temp[k++] = array[j++];
}
int index = first;
k = 0;
while (k < temp.Length)
{
array[index++] = temp[k++];
}
}
快速排序
public void QuickSort(ref int[] array)
{
QkSort(ref array, 0, array.Length - 1);
}
public void QkSort(ref int[] array, int low, int high)
{
int pivot;
if (low < high)
{
pivot = Partition(array, low, high);
QkSort(ref array, low, pivot - 1);
QkSort(ref array, pivot + 1, high);
}
}
private int Partition(int[] array, int low, int high)
{
int key;
key = array[low];
while (low < high)
{
while (low < high && array[high] >= key)
{
high--;
}
swap(ref array, low, high);
while (low < high && array[low] <= key)
{
low++;
}
swap(ref array, low, high);
}
return low;
}
在现实中,待排序的列表极有可能是有序的。固定选第一个为关键字是不合理的做法啊。改进的方法有:
- 三数取中
- 九数取中