产生一个随机整形数组
使用java.util.HashSet集合元素不重复的特点来避免产生重复的元素
实现代码
public static int[] getInt(int size) {
Set s = new HashSet();
int c = 0;
while (c < size) {
int num = (int)(Math.random()*size*100);
s.add(num);
c = s.size();
}
Object[] oArray = s.toArray();
int[] intArray = new int[size];
int i = 0;
for (Object o:oArray) {
int tem = (int)o;
intArray[i++] = tem;
}
return intArray;
算法的基本概念
算法稳定性
元素a=b,在排序前元素a在元素b的前面,排序后如果a仍然位于b的前面则算法具有稳定性,反之则不稳定。
原地算法(in-place)
在计算机科学中,一个原地算法(in-place algorithm)基本上不需要额外辅助的数据结构,然而,允许少量额外的辅助变量来转换数据的算法。当算法运行时,输入的数据通常会被要输出的部分覆盖掉。不是原地算法有时候称为非原地(not-in-place)或不得其所(out-of-place)。–摘自维基百科
冒泡排序 (Bubble Sort)
算法思路
重复扫描数列,每次比较相邻两个元素的大小,小的元素放在左侧,经过一次扫描,数列最大的数位于数列的尾部,视为该元素有序,重复以上工作,直至整个数列有序。
意思就是算法只需要用到O(1)的额外内存空间就可进行
算法描述
- 重复扫描整个数列,扫描次数为数列长度-1。
- 每次扫描(数列的长度-有序元素的个数-1)次。
- 比较两个相邻元素的大小,下标小的存小的元素,下标大的放大的元素。
实现代码
int[] a = RandomNum.getInt(5); //随机数数量
int len = a.length;
for (int i = 0; i < len - 1; i++) {
for(int j = 0; j < len - 1 - i; j++) {
if(a[j] > a[j+1]) {
int z = a[j];
a[j] = a[j+1];
a[j+1] = z;
}
}
}
时间复杂度
- 最好的情况
数列正序,需要判断一轮扫描都没有进行交换,则说明数列排序完毕,那么只需要扫描一遍即可,时间复杂度为O(n)
- 最坏的情况
数列反序,需要扫描数列n-1次,每次扫描需要比较n-i次(1<=i<=n-1),时间复杂度为O(n^2)
算法稳定性
如果两个元素相等且相邻,两个元素不会进行对调,即使不相邻,经过多次交换最终两个相同的元素前后顺序也不会交换,所以冒泡算法是一种稳定排序算法
选择排序(SelectionSort)
算法思路
将数列分为已排序的数列和未排序的数列,每次扫描未排序数列选出最小(最大)的元素,放入已排序数列尾部
算法描述
- 重复扫描数组n-1次
- 每次选出数组最小/最大的元素,置于有序区末尾
实现代码
System.out.println("排序前");
soutIntArray(a);//输出队列元素
int len = a.length;
//外层扫描
for (int i=0; i < len - 1; i++) {
int temp = a[i];
int index = i;
//内层扫描,记录最小元素的值和下标
for (int j = i+1; j < len; j++) {
if (a[j] < temp) {
temp = a[j];
index = j;
}
}
//与当前有序区的元素交换位置
a[index] = a[i];
a[i] = temp;
}
System.out.println("排序后");
soutIntArray(a);
时间复杂度
不管数列如何,选择排序需要扫描n-1次,每次扫描需要比较n-i次(0<=i<=n-1),所以时间复杂度总等于O(n^2)
算法稳定性
如果有两个相同的a、b元素,并且有序区还在a、b元素之前,那么b元素会被调换到a元素之前,所以选择排序不是一种稳定的排序算法
插入排序(Insertion Sort)
算法分析
插入排序通常采用in-place排序,在实现的过程中,因为是从后往前扫,就需要反复的移动已排序的元素,为新插入的元素空出位置。
算法描述
- 选定第一个元素,并视为有序
- 从无序数列中取出下一个元素,在有序数列中从后往前扫描
- 如果新的元素小于比较的元素,则将比较元素后移
- 重复步骤3,直到找到比新元素小的元素并插入到比较元素的前方,或者当比较元素下标小于0时,直接插入下标0处。
- 重复2、3、4步骤,直到无序数列为空
代码实现
int len = a.length;
//默认第一个元素为有序,扫描无序数列
for (int i = 1; i < len; i++) {
int temp = a[i];
//从后往前扫描有序序列
for (int j = i - 1; j < len && j >= 0; j--) {
if(j < 0) {
a[j] = temp;
}
//新的元素比比较元素小,比较元素后移
if (a[j] > temp) {
a[j + 1] = a[j];
a[j] = temp;
continue;
}
//新的元素比比较的元素大则插入其后方
a[j + 1] = temp;
break;
}
}
时间复杂度
- 最好的情况
当数列为正序,每次只需要比较一次即可完成一次插入,总共需要比较n-1次,因此时间复杂度为O(n) 2. 最坏的情况
当数列为反序,每次扫描有序序列需要扫描n-i(i为无序元素个数),总共需要为n-1个无序元素进行排序,因此时间复杂度为O(n^2) 3. *平均情况
无序元素a[k]可能插入的位置有[0,k]个位置,也就是说每个位置被插入的概率均为1/k.
插入无序元素a[k]可能需要比较的次数为[1,k-1]次,平均比较次数为(1+2+3+4+......+k-2+k-1)/k
适用场景
数据量小的时候,或者数列基本有序的时候
希尔排序(Shell Sort)
写入排序是插入排序的改进版,对于中等规模的数据的排序性能表现不错。
算法分析
在插入排序的基础上,新增增量这个概念。对整个数列进行逻辑上分组,分别对每个分组进行插入排序,进行插入排序后,每个分组就为有序数列。缩小增量,继续划分分组,直至最后增量为1,因为此时数组整体有序,因此插入排序的效率较高,同理进行插入排序,完成希尔排序。
增量:按照增量作为间隔距离,对数量进行分组
算法描述
- 确定增量i(1<=i<=n/2)
- 对数列按照增量进行分组
- 对每个分组进行插入排序
- 增量缩小一半
- 重复1、2、3、4直至增量为1
实现代码
//根据增量对原数组进行逻辑分组
while (increment >= 1) {
for (int i = 0; i < len; i++) {
for (int j = i + increment; j < len; j += increment) {
int temp = a[j];
//对每个分组进行插入排序
for (int k = j - increment; k < len && k >= 0; k -= increment) {
if (a[k] < temp) {
a[k+increment] = a[k];
a[k] = temp;
continue;
}
a[k+increment] = temp;
break;
}
}
}
increment /= 2;
}
归并排序(Merge Sort)
采用的是分治的思想,将已有的序列进行分组,分到剩余一个元素,再子序列合并,合并的过程中进行排序。也就是说,先让子序列有序,再使得母序列有序
算法分析
对序列进行分组,直至每个分组剩余一个元素,实现采用递归的方式。完成分组后需要合并分组,合并的时使用两个指针并且需要一个额外的数组保存合并后的序列,两个指针分别指向左右分组的起点,比较两个元素的大小,小的优先插入,当其中一个指针跑到尾部,结束合并,将另一组未写入的元素按顺序写入新的数组中,然后将新的数组的元素值复制给原数组。
算法描述
分组函数
- 判断左右界限是否相同,相同则返回
- 计算下标中位数,mid=L+((R-L)>>1)
- 递归调用本函数,左侧分组的L=L、R=mid。右侧分组的L=mid+1、R=R
- 调用合并函数
合并函数
- 创建额外的数组,数组大小为(R-L+1)
- 使用两个指针指向左右分组的首位元素
- 比较两个指针指向的元素的大小,小的数插入额外的数组中,并且指针后移一位
- 重复3,直至其中一个分组为空
- 将还未写完的分组元素按顺序写入额外数组
- 将额外的数组中的元素按顺序写入原数组中
代码实现
//归并排序
//将数列拆分至单个元素
public static void sort(int a[], int left, int right) {
if (left >= right) {
return;
}
int mid = left + ((right - left) >> 1);
sort(a, left, mid);
sort(a, mid + 1, right);
merge(a, left, mid, right);
}
//合并分组
public static void merge(int a[], int left, int mid, int right) {
//存放合并后的分组
int temp[] = new int[right - left + 1];
//指向两个分组的指针
int p1 = left;
int p2 = mid + 1;
//指向额外数组
int index = 0;
//选择两个分组中较小的元素插入额外数组
while (p1 <= mid && p2 <= right) {
temp[index++] = a[p1] <= a[p2] ? a[p1++] : a[p2++];
}
//将还未写入的分组写入额外数组
while (p1 <= mid) {
temp[index++] = a[p1++];
}
while (p2 <= right) {
temp[index++] = a[p2++];
}
//将额外数组中的内容按照对应的下标写入原来的数组
for (index = 0;left <= right;index++) {
a[left++] = temp[index];
}
}
时间复杂度
对于归并排序,无论数组是哪种状态,时间复杂度都不会变。都是O(log2n)
算法稳定性
上述的实现代码中,选择分组元素进入额外数组时,使用了<=符号,保证了算法的稳定性
快速排序(Quick Sort)
算法分析
快速排序同样也是采用了分治的思想,通过选取基准值,将数列分割成两个分组,比基准小的放在左侧,比基准大的放在右侧,对两个分组继续进行排序,直到最终对整个序列完成排序
算法描述
分组函数
- 调用排序函数,获得基准值
- 将左侧分组代入到排序函数中
- 将右侧分组代入到排序函数中
排序函数
- 选取基准值,默认选取组内的第一个元素作为基准值
- 使用a、b指针分别指向数列的首部和尾部
- 从b指针开始往前扫描碰到比基准值大的元素,尾部指针前移一位。遇到比基准值小的则与a指针交换值,并且a指针后移一位
- a指针往后扫描,遇到比基准值小的值后移一位。遇到比基准值大的数则与b指针交换数值,并且b指针前移一位
- 重复3、4直至a、b指针相遇,此时会发现基准值左侧的元素均小于它,右侧的元素均大于它
- 返回基准值
代码实现
//快速排序
//对两侧数组继续进行排序
public static void quickSort(int a[], int left, int right) {
//当元素个数为1时跳出排序
if (left > right) {
return;
}
int index = partition(a,left,right);
//对左右侧分组进行排序
quickSort(a,left,index-1);
quickSort(a,index+1,right);
}
//已基准值为准,将小于基准值的元素放到左侧,大于基准值的元素放到右侧
public static int partition(int a[], int left, int right) {
//记录基准值大小
int pivot = a[left];
//确定左右指针
int p1 = left;
int p2 = right;
while (p1 != p2) {
while (p1 != p2) {
//扫描右侧指针,如果比基准值大,则指针向前迁移
//如果比基准值小,交换左右指针的值,左指针后移
if (a[p2] <= pivot) {
a[p1++] = a[p2];
break;
}
p2--;
}
while (p1 != p2) {
if (a[p1] >= pivot) {
a[p2--] = a[p1];
break;
}
p1++;
}
}
//两指针相遇后将基准值赋值到相遇的位置,根据上面算法原理,左侧的值都比基准值小,右侧的值都比基准值大
a[p1] = pivot;
return p1;
}
时间复杂度
-
一般情况
-
最差情况
每次基准值选中的都是最大或者最小的值,此时时间复杂度与冒泡相同O(n^2)
算法稳定性
快速排序牺牲了算法的稳定性来换取更高的执行效率,当一个数列中所有元素相同时,如果对于相同的元素均不调换的话,时间复杂度会变为O(n^2)。
堆排序
堆
满二叉树:如果一个二叉树每层的节点数均为最大值,也就是说如果二叉树高度为k,那么该二叉树的节点个数为2^k-1
完全二叉树:一颗高度为k的二叉树,除了第k层外,其余层数节点数均为最大值,且最后一层节点从左到右依次编号,不可中断
堆:堆是具有特殊性的完全二叉树。每个节点的值都大于左右孩子的完全二叉树被称为大顶堆,反之被称为小顶堆
映射到数组中
二叉树映射到数组中 设当前节点的编号为i则得到 a[2i+1] = 左节点编号 a[2i+2] = 右节点编号
算法分析
需要使用到数据结构大顶堆/小顶堆,一般正序用到大顶堆、降序用到小顶堆。(示例使用大顶堆)首先构造大顶堆,将数列放入数组中,从编号最大的不为叶子节点的节点开始,按照该节点最大的原则进行调整,每次调整可能会使子树失去大顶堆性质,因此需要对子树进行递归操作。构造大顶堆后,根节点为当前数列最大的数,将其与最后一个节点交换,交换完后当前状态可能违反大顶堆的性质,此时需要进行调整使其符合大顶堆性质,在调整过程中,需要注意此时有序区+1,树大小-1、调整完毕后,按照该逻辑继续进行,直到整个数列有序
算法逻辑
//构造大顶堆方法
- 获取编号最大的不为叶子结点的节点p的编号=(len/2)向下取整 len/2由子节点公式推得,设当前节点编号为i,左孩子节点=2i+1、右孩子节点=2i+2。取数组长度len也就是最后一个叶子节点的编号,求其父节点即为1中要求的节点p
- 从p节点依次往前调用调整方法
//调整树方法 对树进行调整,使其符合大顶堆性质
- 通过获取到的节点编号计算出左右子树的编号
- 创建额外变量max用于存储三个节点之中值最大的编号,首先将父节点编号的值赋予max
- 判断如果左子树的值大于max指向的值,记录左子树编号到max中
- 判断如果右子树的值大于max指向的值,记录右子树编号到max中
- 判断如果max不等于父节点编号,则调用交换值方法交换两编号中的值,并对交换后的子节点进行递归调用
//交换两节点的值方法
//堆排序方法 交换根节点与对应的节点,调整后继续进行交换,直至完成排序
- 调用构造大顶堆方法,构件大顶堆
- 交换根节点与对应的节点i(len<=i<1),此时被调换的节点属于有序区,所以需要将树的逻辑长度减一
- 调用调整树方法,调整交换后的树,使其重新符合大顶堆性质
- 重复执行1、2直至完成排序
注意点
在构建完大根堆之后交换根节点和对应的节点后,需要将树的逻辑大小减一,因为此时被调换节点已经属于有序区,重新调整树使其符合大根堆性质时,需要排除有序区的元素。
实现代码
//堆排序
//交换元素
public static void swap (int[] a, int father, int child) {
int temp = a[father];
a[father] = a[child];
a[child] = temp;
}
//构件大顶堆
public static void buildMaxHeap(int a[], int len) {
//index指向最后一个非叶子节点的节点
int index = (a.length - 1)/2; //因为java中基本数据类型int中出现的小数点直接舍去,相当于向下取整,符合算法逻辑
//已index为准,依次往前调用保持大顶堆性质的方法,构造出大顶堆
while (index >= 0) {
maxHeapify(a,index--,len);
}
}
//维持大顶堆性质
public static void maxHeapify(int a[], int father,int len) {
//根据父节点算出左右节点的编号
int left = (father*2 + 1);
int right = (father*2 + 2);
//找出三个节点中最大节点的编号
int max = father;
if (left <= len && a[left] > a[max]) {
max = left;
}
if (right <= len && a[right] > a[max]) {
max = right;
}
//如果最大值不为父节点,交换两节点
if (father != max) {
swap(a,father,max);
//交换后子树可能失去大顶堆性质,需要再次递归调用本方法
maxHeapify(a,max,len);
}
}
//堆排序
public static void heapSort(int a[]) {
int len = a.length - 1;
buildMaxHeap(a, len);
//交换根节点与对应的节点n-1次
for (int i = len; i > 0; i--) {
swap(a,0,i);
//交换一次,有序区+1,树的长度-1
maxHeapify(a,0, --len);
}
}