快排:
快排的关键思想是如何划分
当划分后,划分值的左边都是小于等于此值,划分值的右边都是大于此值。然后分别对左边和右边进行递归,直至递归到快排函数中的数只有一位,结束递归,执行后续。
一般的划分思想就是取第一位的值为划分值,然后使用两个指针i ,j 把 i 的值作为第一位的下标,把 j 的值作为最后一位的下标。当 j 下标对应的值大于划分值就把j-- 否则,把把 j 下标的值赋值给i 下标,然后同样的方式看 i 下标。当i ,j下标相等时退出。最后把划分值给i 或 j 下标就行了。
快排程序:
int Partition(int* nums, int left, int right)
{
assert(nums != NULL);
int i = left, j = right;
int tmp = nums[left];
while (i < j)
{
while (i < j && nums[j] > tmp) j--;
if (i < j) nums[i] = nums[j];
while (i < j && nums[i] <= tmp) i++;
if (i < j) nums[j] = nums[i];
}
nums[i] = tmp;
return i;
}
void QuickSort(int* nums, int left, int right)
{
assert(nums != NULL);
if(left < right)
{
int pos = Partition(nums, left, right);
QuickSort(nums, left, pos - 1);
QuickSort(nums, pos + 1, right);
}
}
void QuickSortK(int* nums, int n)
{
assert(nums != NULL);
if (n < 2) return;
else return QuickSort(nums, 0, n - 1);
}
但是当数组本身是有序时,由于划分函数取的是左边第一个元素,因此时间复杂度从o(nlog2n)退化成了o(n^2) ,因此我们要尽可能让他们取中间,这就需要重新编写划分函数。
这里我只会两种方法:三位取中法和随机取值法
随机取值法:
int RandomPartition(int* nums, int left, int right)
{
assert(nums != NULL);
srand(time(nullptr));
int tmp = rand() % (right - left + 1) + left;
std::swap(nums[left], nums[tmp]);
return Partition(nums, left, right);
}
三位取中法:
思想:
mid = (right - left + 1)/ 2 + left;
比较left ,mid ,right 这三个数哪个是中间值,返回他的下标,但是下标不好找,因此定义一个结构体
struct Index
{
int val ;
int index;
}
函数表示:
typedef struct Index
{
int val;
int index;
}Ind;
int CompareMin(int n, int m)
{
return n < m ? n : m;
}
int CompareMax(int n, int m)
{
return n > m ? n : m;
}
int MedionThree(int* nums, int left, int right)
{
int mid = (right - left) / 2 + left;
if (mid == left) return mid;
struct Index idx[] = { {nums[left] , left} ,{nums[mid] ,mid} ,{nums[right] ,right} };
int min = CompareMin(idx[0].val, CompareMin(idx[1].val, idx[2].val));
int max = CompareMax(idx[0].val, CompareMax(idx[1].val, idx[2].val));
for (int i = 0;i < 3; i++)
{
if (idx[i].val != min && idx[i].val != max)
{
return idx[i].index;
}
}
}
void QuickSort(int* nums, int left, int right)
{
assert(nums != NULL);
if(left < right)
{
int pos = MedionThree(nums, left, right);
QuickSort(nums, left, pos - 1);
QuickSort(nums, pos + 1, right);
}
}
void QuickSortK(int* nums, int n)
{
assert(nums != NULL);
if (n < 2) return;
else return QuickSort(nums, 0, n - 1);
}
归并排序:
归并排序的思想是开辟一个和数组等大的空间把数组按顺序合并到一起
类似于这种:
我们就需要用到分治的思想,把数组划分到直至只有一个数,然后比较两个划分数组中哪个数更小,优先放入开辟的指定空间中。最后在从开辟的空间中复制到原空间中。
代码实现:
void Copy(int* dest, int* src, int left, int right) // 第一个是开辟的空间,第二个是原空间
{
for (int i = left; i <= right; i++)
{
dest[i] = src[i];
}
}
void Merge(int* dest, int* src, int left, int right, int mid)
{
int i = left, j = mid + 1;
int k = left;
while (i <= mid && j <= right)
{
if (src[i] < src[j]) dest[k++] = src[i++];
else dest[k++] = src[j++];
}
while (i <= mid) dest[k++] = src[i++];
while (j <= right) dest[k++] = src[j++];
}
void MergeSort(int* dest, int* src, int left, int right)
{
assert(src != NULL);
int mid = (right - left) / 2 + left;
if (left < right)
{
MergeSort(dest, src, left, mid);
MergeSort(dest, src, mid + 1, right);
Merge(dest, src, left, right, mid);
Copy(src, dest, left, right);
}
}
void MergePass(int* nums, int n)
{
assert(nums != NULL);
if (n < 2) return;
int* tmp = (int*)malloc(sizeof(int) * n);
memset(tmp, 0, sizeof(int) * n);
MergeSort(tmp, nums, 0, n - 1);
free(tmp);
}
int main()
{
int ar[] = { 45,23,56,87,12,43,78,90,70 };
int n = sizeof(ar) / sizeof(ar[0]);
MergePass(ar, n);
for (int i = 0; i < n; i++)
{
printf("%2d ", ar[i]);
}
return 0;
}
归并排序的时间复杂度也是o(nlog2n),但是空间复杂度要比其他两个排序大,为o(n)
归并排序也可以使用非递归形式实现
思想:
先从 s = 1时开始相邻和相邻的归并,然后s += s; 因此,每次合并都是2的幂次方个,到s = 2,就像这里不足2的幂次方时,就直接拷贝下来,参与后面的归并。
函数表示:
void Merge(int* dest, int* src, int left, int right, int mid)
{
int i = left, j = mid + 1;
int k = left;
while (i <= mid && j <= right)
{
if (src[i] < src[j]) dest[k++] = src[i++];
else dest[k++] = src[j++];
}
while (i <= mid) dest[k++] = src[i++];
while (j <= right) dest[k++] = src[j++];
}
void Merge_Non(int* dest, int* src, int n ,int s)
{
int i = 0;
while (i + s*2 -1 <= n-1) // 看够不够对应s合并所需要的下标
{
Merge(dest, src, i, i + s * 2 - 1, i + s - 1);
i = i + s * 2;
}
if (n - 1 >= i + s)
{
Merge(dest, src, i, n - 1, i + s - 1);
}
else
{
for (int j = i; j < n; j++)
{
dest[j] = src[j];
}
}
}
void Merge_Nonrecur(int* nums ,int n)
{
int* tmp = (int *)malloc(sizeof(int)*n);
memset(tmp, 0, sizeof(int) * n);
int s = 1;
while (s < n)
{
Merge_Non(tmp , nums ,n ,s);
s += s;
Merge_Non(nums ,tmp , n ,s);
s += s;
}
free(tmp);
}
堆排序
堆排序的思想:
1 先变成最小堆,这时候根节点就为最小
2 再把第一位(根节点)与最后一位换一下,这时候最后一位就是最小值
3 再从0 到倒是第二位变成最小堆,再与倒数第二位交换,从0 到倒数第三位变成最小堆,重复这种步骤。
4 就这样一直到最后一位(第0 位)
那么如何变成最小堆呢?
首先找最后一位下标的双亲结点,和7下标对应的值进行比较,如果7下标的值小于3下标的值,交换7下标与3下标的值。否则,看2下标,比较2下标与其子节点大小,如果2下标不是最小的,就交换成最小的。然后再看1下标,比较与其子节点大小。然后再看0节点。
怎么进行交换呢?首先把该节点的值赋值给tmp ,然后找到小的子节点,再和该节点比较。如果大于此节点,直接退出。如果小于此节点,就把小节点的值赋值给该节点。然后再看那个较小节点的子节点,按同样方法比较,直至到没有子节点为止。
代码实现:
void FilterDown(int* nums, int start, int end) // 这里的start是双亲节点,end是子节点
{
int i = start, j = i * 2 + 1; // 双亲结点和子节点关系 j = i * 2 + 1
int tmp = nums[start];
while (j <= end)
{
if (j + 1 <= end && nums[j] > nums[j + 1])
{
j = j + 1;
}
if (nums[j] >= nums[i]) break;
else
{
std::swap(nums[j], nums[i]);
i = j;
j = j * 2 + 1;
}
}
nums[i] = tmp;
}
void HeapSort(int* nums, int n)
{
assert(nums != NULL);
if (n < 2) return;
int pos = (n - 2) / 2;
while (pos >= 0)
{
FilterDown(nums, pos, n - 1);
pos--;
}
pos = n - 1;
while (pos > 0)
{
std::swap(nums[0], nums[pos]);
pos--;
FilterDown(nums, 0, pos);
}
}
当要想从小到大排序,只需要将 FilterDown中的大于改成小于就行了。