【数据结构】排序和查找

排序算法

稳定排序

稳定与非稳定指的是:在一串数列中,…ai…aj…,ai==aj,在经过排序后,他俩的相对位置不变,则为稳定排序。

插入排序

将我们的整个待排序数组分成两个区域,一个叫已排序区,一个叫待排序区。

已排序区在前方,待排序区在后方。

每次将待排序区中的第一个元素取出来,在已排序区中进行比较,逐个交换位置,找到符合条件的位置进行放入。

形象点可以理解为:

  1. 一个人排队的时候一直往前拱,每次拱的时候,正前方那位向后移动一位(交换)。
  2. 遇到老大哥的时候害怕了不敢继续往前了,于是就在那呆着了。

举例说明:

2 1 4 9 8 5 3

第一轮:

没有已排序的,全是待排序区,第一步将2放入已排序区的队首。

2(已) 1 4 9 8 5 3

第二轮:

未排序区的队首1 和前面的比较,发现1<2,向前拱

1(已) 2(已) 4 9 8 5 3

第三轮:

未排序区的队首4 和前面的比较,发现前面2比自己小,所以不拱:

1(已) 2(已) 4(已) 9 8 5 3

第四轮:

未排序区的队首4 和前面的比较,发现前面4比自己小,所以不拱:

1(已) 2(已) 4(已) 9(已) 8 5 3

第五轮:

未排序区的队首8 和前面的比较,8<9,向前拱。然后再和前面的4比较,发现前面4比自己小,所以不拱:

1(已) 2(已) 4(已) 8(已) 9(已) 5 3

第六轮:

未排序区的队首5 和前面的比较,5<9,向前拱。然后再和前面的8比较,5<8,向前拱…这里一共交换了两次:

1(已) 2(已) 4(已) 5(已) 8(已) 9(已) 3

第七轮:

3<9,交换;3<8,交换…最后成功排序。

代码说明:

/**
* 插入排序
*/
void insert_sort(int* num, int n) {
    
    
	// 假设将第一个元素看作是已排序区的队首
	// 那么后面只需要进行 n - 1 轮排序
	for (int i = 1; i < n; ++i) {
    
    
		// 每一轮排序,都是待排序区的第一个和前面已排序区的进行挪动比较
		for (int j = i; j > 0; --j) {
    
    
			if (num[j] < num[j - 1]) {
    
    
				swap(num[j], num[j - 1]);
			}
		}
	}
	return;
}

冒泡排序

冒泡排序的已排序区在后方,待排序区在前方。

  1. 一个起指针作用的数指向待排序区的第一个元素
  2. 每次将待排序区的第一个元素和后面的元素进行比较,如果符合交换条件,则交换,并且指针后移一位
  3. 当指针循环向后移动到待排序区的最后一个元素时,待排序区里的最值已经挪到了最后一个位置上,形成了已排序区的新的首元素。

现在进行时间复杂度分析:

如果数组是有序的,那么这个头部指针遍历一次后没有交换,一遍就完成了

如果是逆序的,那么操作是n^2

举例说明:

1 2 5 4 7 3 6 10 8 9

第一轮:

最后一个元素作为已排序区

1 2 5 4 7 3 6 10 8 9~~(已)~~

第二轮:

第一轮:

指针放在未排序区的队首,逐个向后挪动。

在挪动的过程中如果发现指针指向的元素比后面那个元素大,则将元素进行交换。

然后指针继续向后挪动。

1(↓) 2 5 4 7 3 6 10 8 9

1<2,不交换。

同样,2<5也不交换,现在指针来到第三个位置。

1 2 5(↓) 4 7 3 6 10 8 9

5>4,进行交换

1 2 4 5(↓) 7 3 6 10 8 9

5<7,不交换,指针来到第五个位置。

1 2 4 5 7(↓) 3 6 10 8 9

7>3,进行交换

1 2 4 5 3 7(↓) 6 10 8 9

指针在第六个位置的时候发现7>6,交换。

然后10和8交换,10再和9交换,最后,10挪到最后一个位置,停止交换,准备开始第二轮。

代码展示:

/**
* 冒泡排序
*/
void bubble_sort(int* num, int n) {
    
    
	// 冒泡排序也是执行 n - 1 轮
	for (int i = 1; i < n; ++i) {
    
    
		// 每次指针从头开始,随着j的变化,逐个比较并交换两相邻元素
		// 需要注意的是,如果传递进来的是10个元素,那么最大的下标为 10 - 1
		for (int j = 0; j < n - i ; ++j) {
    
    
			if (num[j] > num[j + 1]) {
    
    
				swap(num[j], num[j + 1]);
			}
		}
	}
	return;
}

优化

在某轮排序的过程中,如果没有发生交换的操作,这就意味着前面的待排序区已经是有序的状态。

具体的处理操作就是,定义一个times变量,负责记录指针扫描时候的交换次数。

/**
* 优化后的冒泡排序
*/
void bubble_sort(int* num, int n) {
    
    
	int times = 1;
	for (int i = 1; i < n && times; ++i) {
    
    
		times = 0;
		for (int j = 0; j < n - i ; ++j) {
    
    
			if (num[j] > num[j + 1]) {
    
    
				swap(num[j], num[j + 1]);
				times++;
			}
		}
	}
	return;
}

归并排序

首先,假如如果由两个有序数组(n个和m个)合并,我们需要多少次操作呢?

很简单,拿两个指针指向他们的头部,然后再开辟一个新的存储区,如果谁指向的数据小,谁对应的数据就放到存储区中去。

好了,现在再来看看归并排序。

它采用了一种大事化小,小事化了的思想。

  1. 如果提供了一个长度为10的数组,那我就可以将他们分为两个长度为5的子数组。如果这两个数组已经有序了,那么是否就是上面说的,O(m+n)的合并操作呢?
  2. 可能长度为5还不够小,所以我们可以继续分,分为长度为[2,3 | 2,3]这四个数组
  3. 先将两个小数组进行排序,成为了有序数组后再进行合并

现在进行时间复杂度分析:

每一层都对小的数组进行排序,最少进行n次操作。

不难看出,这个划分的过程实现了一个二叉树,树的层高为log2^n。

所以总操作次数为nlog2^n

举例说明:

9 7 8 6 4 3 1 2 7 5

首先将数组进行拆分:

A:

9 7 8 6 4

B:

3 1 2 7 5

继续拆分:

A:

9 7

B:

8 6 4

C:

3 1

D:

2 7 5

然后继续拆分,这里就不再演示。

之后对于每个细分为仅1个或者2个的小数组进行比较排序

排完序后将有序的小数组进行整合,整合为一个大的数组。

代码展示:

/**
* 归并排序
*/
void merge_sort(int* num, int l, int r) {
    
    
	// 划分后只剩一个元素,直接返回;剩余两个元素,比较后再返回
	if (r - l <= 1) {
    
    
		// 此时划分后的小数组中刚好两个元素
		if (r - l == 1 && num[l] > num[r]) {
    
    
			swap(num[l], num[r]);
		}
		return;
	}
	// 计算中间结点,按照中间结点进行数组的划分
	int mid = (l + r) >> 1;
	merge_sort(num, l, mid);
	merge_sort(num, mid + 1, r);
	// 开辟空间存放新的排序好的子数组
	int* temp = (int*)malloc(sizeof(int) * (r - l + 1));
	// 两个子数组合并时
	// p1指向左边的起始位置
	// p2指向右边的起始位置(r表示的是最右边,mid + 1表示的是右边的起始
	// temp指向临时存储区的起始位置
	int p1 = l, p2 = mid + 1, k = 0;
	while (p1 <= mid || p2 <= r) {
    
    
		if ((p1 <= mid && num[p1] < num[p2]) || p2 > r) {
    
    
			temp[k++] = num[p1++];
		}
		else {
    
    
			temp[k++] = num[p2++];
		}
	}
	// memcpy(num + l, temp, sizeof(int) * (r - l + 1));
    
    // 上面这句话这样写会好理解一点
    // 假设现在是长度[2,2]的子数组合并为4,每次合并完temp后都需要修改原数组的值
    // 这样跳回到上一层的递归中时,仍然可以保留修改后的数据值
    for (int i = l,j = 0; i <= r; ++i, ++j) {
    
    
		num[i] = temp[j];
	}
	free(temp);
}

不稳定排序

选择排序

这里复习一下前面的插入排序。

在插入排序中,未排序区向已排序区中去拱,在逐个交换的过程中放到一排序区中去。

而在选择排序中,这个方向是相反的:

指针指向未排序区的第一个元素,每次遍历一次未排序区,将符合条件的元素和队首去进行一次交换,然后指针后移进入下一轮

代码展示:

/**
* 选择排序
*/
void select_sort(int* num, int n) {
    
    
	for (int i = 0; i < n; ++i) {
    
    
		int tempIndex = i;
		for (int j = i + 1 ; j < n; ++j) {
    
    
			tempIndex = (num[j] < num[tempIndex]) ? j : tempIndex;
		}
		swap(num[i], num[tempIndex]);
	}
}

快速排序

  1. 首先设置一个选择基数
  2. 进行partition操作,前半段小于基准值,后半段大于等于基准值

举例说明:

5 7 8 6 4 3 1 2 7 5
  1. 首先设置基准值

​ 基准值:[ 5 ]

(head) 7 8 6 4 3 1 2 7 5 (tail)
  1. 尾指针向前走,找到一个小于5的值,将这个值放入开始的空位置

​ 找到元素[ 2 ],放入头指针位置

2(head) 7 8 6 4 3 1 (tail) 7 5
  1. 此时头指针后移,找到第一个大于基准值的元素,放到尾指针去
2 (head) 8 6 4 3 1 7(tail) 7 5
  1. 再将尾指针前移…当头尾指针重合的时候,将基准值放到这个位置上
  2. 进行递归操作,对左右两侧递归的使用快速排序

代码展示:

/**
* 快速排序
* 需要两个指针负责控制交换
* 同时对两端的数据进行递归操作
*/
void quick_sort(int* num, int l, int r) {
    
    
	if (r <= l) return;
	int x = l, y = r;
	int temp = num[l];	// 设置基准值
	// 左右指针没有相遇
	while (x < y) {
    
    
		while (x < y && num[y]>temp) --y;
		if (x < y) num[x++] = num[y];
		while (x < y && num[x]<temp) ++x;
		if (x < y) num[y--] = num[x];
	}
	// 此时左右指针重合,将基准值放入重合的位置
	num[x] = temp;
	// 左右递归快排
	quick_sort(num, l, x - 1);
	quick_sort(num, x + 1, r);
	return;
}

查找算法

二分查找

首先的首先,二分查找基于一个有序的数组。

如果arr[mid]>目标值,那么就去左半边查找;否则去右半边

代码展示:

/**
* 二分查找
* 有序数组的查找问题
*/
int binary_search(int* num, int n, int x) {
    
    
	int head = 0, tail = n - 1, mid;
	while (head <= tail) {
    
    
		mid = (head + tail) >> 1;
		if (num[mid] == x) return mid;
		if (num[mid] > x) 
			tail = mid - 1;
		else 
			head = mid + 1;
	}
}

这里对二分查找进行一次延伸:

设数组为[1,1,1,1,1,1,1,0,0,0],要找到这个数组中最后一个1所在的下标。

int binary_search(int* num, int n) {
    
    
	int head = 0, tail = n - 1, mid;
    // 这里就不能带等号了
	while (head < tail) {
    
    
		mid = (head + tail) >> 1;
		if (num[mid] == 1) head = mid;
		else tail = mid - 1;
	}
	return head;
}

猜你喜欢

转载自blog.csdn.net/flow_camphor/article/details/125052285