数据结构入门、排序算法
如果不接触一段时间的算法,真的很容易就忘记了,经历过校招的人都知道,算法和数据结构都是不可避免的,在面试和笔试的时候都会有遇到。
刚刚接触学习的同学,我们可以先以排序和各种数据结构入门。
冒泡排序
冒泡排序是一种简单的排序算法,它重复的走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来,走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越大的元素会经交换慢慢“浮“到数列顶端,像冒泡一样。
冒泡排序的思想就是两两交换,大的放在数组后面,第一次排序后最大值已在数组末尾。因为两两交换,需要n-1趟排序(比如10个数,需要9趟排序)
代码实现要点:两个for循环,外层控制排序的趟数,内层控制循环比较的次数,每趟过后,比较的次数都应该要减1。
BubbleSort(int[] arrays){
//装载临时变量
int temp;
//检查是否发生置换,0表示没有,1表示有
int ischange;
//记录执行了多少趟
int num=0;
//外层循环是排序的次数
for(int i=0;i<arrays.length-1;i++){
//每排序一趟置换为0
ischange = 0;
//内置循环是当前趟数需要比较的次数
for(int j=0;j<arrays.length-i-1;j++){
//如果前一位与后一位比较,前一位比后一位大就交换
if(arrays[j]>arrays[j+1]){
temp = arrays[j];
arrays[j]=arrays[j+1];
arrays[j+1]=temp;
//发生了置换 变为1
ischange =1;
}
}
//如果比较完一趟没有发生置换,证明已经排好序了,不需要再继续执行
if(ischange==0){
break;
}
num++;
}
//遍历数组
showArray(arrays);
System.out.println(num+"次");
}
public void showArray(int[] arrays){//遍历数组方法
for(int i:arrays){
System.out.print(i+" ");
}
}
直接选择排序
直接选择排序是一种简单直观的排序算法,它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始(末尾)位置,直到全部待排序的数据元素排完。
直接选择排序是不稳定的,比如序列(5,5,3)第一次就将第一个5和第一个3交换,导致第一个5挪动到第二个5后面。
我们判断某排序算法是否稳定,可以简单理解成:排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。
- 如果相同,则是稳定的排序方法
- 如果不相同,则是不稳定的排序方法
稳定排序的好处就是:如果我们只对一串数字排序,那么稳定与否确实不重要,因为一串数字的属性是单一的,就是数字值得大小。但是排序的元素往往不只有一个属性,例如我们对一群人按年龄排序,但是人除了年龄属性还有身高体重属性,在年龄相同时如果不想破坏原先身高体重的次序,就必须用稳定排序算法。
直接选择排序的思路是找到数组中最大的元素,与数组最后一位元素交换。当只有一个数时,则不需要选择了,因此需要n-1趟排序。
代码实现的要点:两个for循环,外层循环控制排序的趟数,内层循环找到当前趟数的最大值,随后与当前趟数组最后的一位元素交换。
SelectSort(int[] arrays) {
//记录当前趟数的最大值的标志
int index;
//交换的变量
int temp;
//外层循环控制需要排序的趟数
for (int i = 0; i < arrays.length - 1; i++) {
//新的趟数 将标志归0
index = 0;
//内层循环控制遍历数组的个数并得到最大值的标志
for (int j = 0; j < arrays.length - i; j++) {
if (arrays[j] > arrays[index]) {
index = j;
}
}
//交换
temp = arrays[index];
arrays[index] = arrays[arrays.length - i - 1];
arrays[arrays.length - i - 1] = temp;
}
showArray(arrays);
}
public void showArray(int[] arrays) {//遍历数组方法
for (int i : arrays) {
System.out.print(i + " ");
}
}
插入排序
插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的,个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2).是最稳定的排序方法。
插入排序的思路:将一个元素插入到已有序的数组中,在初始时未知是否存在有序的数据,因此将元素第一个元素看成是有序的。与有序的数组进行比较,比它大的直接放入,比它小则移动数组元素的位置,找到个合适的位置插入。当只有一个数时,则不需要插入了,因此需要n-1趟排序。
代码实现:一个for循环内嵌一个while循环实现,外层for循环控制需要排序的趟数,while循环找到合适的插入位置(并且插入的位置不能小于0)
InsertSort(int[] arrays) {
//临时变量
int temp;
//外层循环控制需要排序的趟数(从1开始因为将第0位看成了有序数据)
for (int i = 1; i < arrays.length; i++) {
temp = arrays[i];
//如果前一位(已排序的数据)比当前数据要大,那么就进入循环比较
while (i >= 1 && arrays[i - 1] > temp) {
//往后退一个位置,让当前数据与之前前位进行比较
arrays[i] = arrays[i - 1];
//一直往前比较,直到退出循环
i--;
}
//退出循环说明找到了合适的位置了,将当前数据插入到何时的位置中
arrays[i] = temp;
}
showArray(arrays);
}
快速排序
快速排序的基本思想是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分布进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
思路:在数组中找一个元素(节点),比它小的放在节点的左边,比它大的放在节点右边。一趟下来,比节点小的在左边,比节点大的在右边。不断执行这个操作。
代码实现:支点取中间,使用L和R表示数组的最小和最大位置。不断进行比较,直到找到比支点小的数,随后交换,不断减小范围。递归L到支点前一个元素(j)。递归支点后一个元素(i)到R元素。
public static void QuickSort(int[] arrays, int L, int R) {
int i = L;
int j = R;
//支点
int pivot = arrays[(L + R) / 2];
//左右两端进行扫描,只要两端还没有交替,就一直扫描
while (i <= j) {
//寻找直到比支点大的数
while (pivot > arrays[i]) {
i++;
}
//寻找直到比支点小的数
while (pivot < arrays[j]) {
j--;
}
//此时已经分别找到了比支点小的数(右边)、比支点大的数(左边),它们进行交换
if (i <= j) {
int temp = arrays[i];
arrays[i] = arrays[j];
arrays[j] = temp;
i++;
j--;
}
}
//上面一个while保证了第一趟排序支点的左边比支点小,支点的右边比支点大了
//左边再做排序,直到左边剩下一个数(递归出口)
if (L < j) {
QuickSort(arrays, L, j);
}
if (i < R) {
QuickSort(arrays, i, R);
}
}
归并排序
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序思路:将两个已排好序的数组合并成一个有序的数组。将元素分隔开来,看成是有序的数组,进行比较合并。不断拆分和合并,直到只有一个元素。
代码实现:在第一趟排序时实质是两个元素(看成是两个已有序的数组)来进行合并,不断执行这样的操作,最终数组有序,拆分左边,右边,合并。
public static void MergeSort(int[] arrays, int L, int R) {
//如果只有一个元素,那就不用排序了
if (L == R) {
return;
} else {
//取中间的数,进行拆分
int M = (L + R) / 2;
//左边的数不断进行拆分
MergeSort(arrays, L, R);
//右边的数不断进行拆分
MergeSort(arrays, M + 1, R);
//合并
Merge(arrays, L, M + 1, R);
}
}
/*
L指向数组第一个元素
M指向数组分隔的元素
R指向数组最后的元素
*/
public static void Merge(int[] arrays, int L, int M, int R) {
//左边的数组的大小
int[] leftArray = new int[M - L];
//右边的数组的大小
int[] rightArray = new int[R - M - 1];
//往这两个数组填充数据
for (int i = L; i < M; i++) {
leftArray[i - L] = arrays[i];
}
for (int i = M; i <= R; i++) {
rightArray[i - M] = arrays[i];
}
int i = 0, j = 0;
//arrays数组的第一个元素
int k = L;
//比较这两个数组的值,哪个小,就往数组上放
while (i < leftArray.length && j < rightArray.length) {
//谁比较小,谁将元素放入大数组中,移动指针,继续比下一个
if (leftArray[i] < rightArray[j]) {
arrays[k] = leftArray[i];
i++;
k++;
} else {
arrays[k] = rightArray[j];
j++;
k++;
}
}
//如果左边的数组还没比较完,右边的数都已经完了,那么将左边的数抄到大数组中(剩下的都是大数字)
while (i < leftArray.length) {
arrays[k] = leftArray[i];
i++;
k++;
}
//如果右边的数组还没比较完,左边的数都已经完了,那么将右边的数抄到大数组中(剩下的都是大数字)
while (j < rightArray.length) {
arrays[k] = rightArray[j];
k++;
j++;
}
}
堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。
思路:堆排序使用到了完全二叉树的一个特性,根节点比左孩子和右孩子都要大,完成一次建堆的操作实质上是比较根节点和左孩子、右孩子的大小,大的交换到根节点上,直至最大的节点在树顶。随后与数组最后一位元素进行交换。
代码实现:只要左子树或右子树大于当前根节点,则替换。替换后会导致下面的子树发生了变化,因此同样需要进行比较,直至各个节点实现父>子这么一个条件。
{
for (int i = 0; i < arrays.length; i++) {
maxHeapify(arrays, arrays.length - i);
//交换
int temp = arrays[0];
arrays[0] = arrays[(arrays.length - 1) - i];
arrays[(arrays.length - 1) - i] = temp;
}
}
希尔排序
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。
思路:希尔排序实质上就是插入排序的增强版,希尔排序将数组分隔成n组来进行插入排序,直至该数组宏观上有序,最后再进行插入排序时就不用移动那么多次位置了。
代码思路:希尔增量一般是gap = gap / 2,只是比普通版插入排序多了这么一个for循环而已。
public ShellSort(int[] arrays) {
for (int step = arrays.length / 2; step > 0; step /= 2) {
for (int i = step; i < arrays.length; i++) {
for (int j = i - step; j >= 0 && arrays[j] > arrays[i]; j -= step) {
int temp = arrays[j];
arrays[j] = arrays[i];
arrays[i] = temp;
}
}
}
showArray(arrays);//遍历数组
}
基数排序
基数排序属于“分配式排序”,又称“桶子法”,它是透过键值的部分资讯,将要排序的元素分配至某些“桶”中,籍以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O(nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其他的稳定性排序法。
思路:基数排序(桶排序):将数字切割成个、十、百、千位放入到不同的桶子里,放入一次就按桶子顺序回收一次,直至最大位数的数字放完~那么该数组就有序了。
代码实现:先找到数组的最大值,然后根据最大值/10来作为循环的条件(只要>0,那么就说明还有位数)。将个位、十位…分配到桶子里,每分配一次就回收一次。
public static int findMax(int[] arrays, int L, int R) {
if (L == R) {
return arrays[L];
} else {
int a = arrays[L];
int b = findMax(arrays, L + 1, R);
if (a > b) {
return a;
} else {
return b;
}
}
}
public static void radixSort(int[] arrays) {
int max = findMax(arrays, 0, arrays.length - 1);
//需要遍历的次数由数组最大值的位数来决定
for (int i = 1; max / i > 0; i = i * 10) {
int[][] buckets = new int[arrays.length][10];
//获取每一位数字(个、十、百。。。分配到桶子里
for (int j = 0; j < arrays.length; j++) {
int num = (arrays[j] / i) % 10;
//将其放入桶里
buckets[j][num] = arrays[j];
}
//回收桶子里的元素
int k = 0;
//有10个桶子
for (int j = 0; j < 10; j++) {
//对每个桶子里的元素进行回收
for (int l = 0; l < arrays.length; l++) {
//如果桶子里面有元素就回收(数据初始化为0)
if (buckets[l][j] != 0) {
arrays[k++] = buckets[l][j];
}
}
}
}
}
稳定的排序:插入排序、冒泡排序、归并排序、基数排序
不稳定的排序:希尔排序、选择排序、堆排序、快速排序
最后
想要说明的是,排序算法/数据结构的代码可能不是最优解,代码的实现都是以比较容易理解的方式去写的。几乎每句都有对应的注释,应该是能看懂的。
有什么出错的地方做的不好的地方,希望各位大佬能够多多体谅,指点一下。