本系列为C++算法学习系列,会介绍 算法概念与描述,入门算法,基础算法,数值处理算法,排序算法,搜索算法,图论算法, 动态规划等相关内容。本文为排序部分。
【 3 】排序的基本概念 【 3 】冒泡排序 【 3 】选择排序 【 3 】插入排序 【 3 】计数排序
参考;https://www.cnblogs.com/itsharehome/p/11058010.html
1、排序的概念:
就是将一组杂乱无章的数据按照一定的规律(升序或者降序)组织起来
2、排序码
通常数据元素有多个属性域,其中有一个属性域可用来区分元素,作为排序依据,该域即为排序码 作为排序依据的数据项称为“排序码”,也即数据元素的关键码。若关键码是主关键码,则对任意待排序序列,经排序后得到的结果是唯一的;若关键码是次关键码,排序结果可能不唯一,这是因为具有相同关键码的数据元素,这些元素再排序结果中,它们之间的位置关系与排序前不能保持。
3、术语解释 稳定排序、原地排序、时间复杂度、空间复杂度:
1、稳定排序:如果 a 原本在 b 的前面,且 a==b,排序之后 a 仍然在 b 的前面,则为稳定排序。 2、非稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 可能不在 b 的前面,则为非稳定排序。 3、原地排序:原地排序就是指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。 4、非原地排序:需要利用额外的数组来辅助排序。 5、时间复杂度:一个算法执行所消耗的时间。 6、空间复杂度:运行完一个算法所需的内存大小。
4、内部排序
数据元素全部放在内存中的排序
5、外部排序
数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。 常见的内部排序算法有: 插入排序: 插入排序(Insertion Sort)、希尔排序(Shell Sort) 选择排序: 选择排序(Selection Sort)、堆排序(Heap Sort) 交换排序: 冒泡排序(Bubble Sort)、快速排序(Quick Sort) 归并排序(Merge Sort) 基数排序(Radix Sort) 计数排序(Counting Sort) 桶排序(Bucket Sort)
用一张图概括:
In-place:占用常数内存,不占用额外内存 Out-place:占用额外内存
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有元素再需要交换,也就是说该数列已经排序完成。 这个算法的名字由来是因为越小的元素会经由交换像气泡一样慢慢“浮”到数列的顶端。
每次比较相邻的元素,如果第一个比第二个大,就交换他们两个。对每—对相邻元素做同样的工作,从开始第一对到结尾的最后一对。经过一轮排序后,最后的元素应该会是最大的数。 针对所有的元素重复以上的步骤,除了最后一个。 持续每轮对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较,也就是已经是按照从小到大的顺序排列了。
每轮比较都会确定一个数字的位置,因此N个数字需要比较N-1轮。如如果是5个数比较,则
第一轮比较了4次, 第二轮比较3次, 第三轮比较2次, 第四轮比较1次, 那么第i轮比较的次数为N-i次。 每次比较均是对相邻两个数字作比较,直至最后。
#include <iostream>
using namespace std;
int main()
{
int n;
int a[6]={0,3,4,1,5,2};
n=sizeof(a)/sizeof(int);
cout<<n<<endl;
int outres=0;
for(int i=1;i<=n;i++)
{
outres++;
int innerres=0;
for(int j=1;j<=n-i;j++)
{
innerres++;
if(a[j]>a[j+1])
{
swap(a[j],a[j+1]);
}
cout<<"执行的外循环次数-->"<<outres<<"执行的内循环次数-->"<<innerres<<endl;
}
}
for(int i=1;i<=5;i++)
{
cout<<a[i]<<" ";
}
return 0;
}
参考:冒泡排序的三种优化 刚才对于序列{12,35,99,18,76}的排序过程中,我们不难发现,第二轮排序进行完之后,整个序列已经是有序的了,也就是说第二轮排序结束就可以不用接着进行接下来的比较了。
因此我们可以对刚才的程序进行优化,那么什么时候就可以结束排序过程呢?根据观察,我们发现当某轮排序过程中没有交换的发生,那么就说明序列已经有序,无需再次比较了。
#include <iostream>
using namespace std;
int main()
{
int n;
int a[6]={0,3,4,1,5,2};
n=sizeof(a)/sizeof(int);
cout<<n<<endl;
int outres=0;
for(int i=1;i<=n;i++)
{
bool flag=0;// 优化 标记是否有交换
outres++;
int innerres=0;
for(int j=1;j<=n-i;j++)
{
innerres++;
if(a[j]>a[j+1])
{
flag=1;//优化 有交换标记1
swap(a[j],a[j+1]);
}
cout<<"执行的外循环次数-->"<<outres<<"执行的内循环次数-->"<<innerres<<endl;
}
if(flag==0)break;//优化 有交换标记1
}
for(int i=1;i<=5;i++)
{
cout<<a[i]<<" ";
}
return 0;
}
如:
过程简单描述:
首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。其次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。这种方法我们称之为选择排序。
(从前向后,升序)每次从边界出发,第一个元素作为擂主(不动),和第二个元素进行比较,如果擂主小,那么不交换值,cur 继续向后走,下一个值继续和擂主比,直到最后一个元素和擂主比较完,这个时候第二个元素称为擂主,从第三个元素开始比较,直到比到最后。每一次打擂台都有可能打乱原有符合排序规则元素之间的原有位置,所以不稳定。
#include<iostream>
using namespace std;
// 选择排序 升序
// [0, bound) 是有序区间
// [bound, size) 是待排序区间
void SelectSort(int arr[], int size)
{
if(arr == NULL || size <= 0){
return;
}
int bound = 0;
for(; bound < size; bound ++){
int cur = bound + 1;
for(; cur < size; cur ++){
if(arr[bound] > arr[cur]){
swap(arr[bound], arr[cur]);
}
}
}
return;
}
int main(){
int a[10]{ 5,7,9,6,3,1,4,8 };
SelectSort(a, 8);
for (int i = 0; i < 8; i++)
{
cout << a[i]<<" ";
}
}
插入排序(Insertion sort)是一种简单直观且稳定的排序算法。如果有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法—插入排序法。 插入排序的基本操作就是将一个数据插入到已经排好序的有序数列中,从而得到一个新的、个数加一的有序数列,算法适用于少量数据的排序。
1、从第一个元素开始,该元素被认为已被排序。 2、取出下一个元素,在已排序的序列中从后往前扫描。 3、如果该元素大于新元素,将该元素移到下一个位置。 4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。5、将新元素插入后,重复步骤2~5。
接下来我们以对序列{5,6,3,7,8,1}从小到大排序为例来讲解插入排序的具体过程。
第一步:有序序列为{5}。 第二个数6开始进行插入排序。因为5是小于6的,所以位置不 用改动。在第二个位置插入数字6,得到有序序列{5,6}。 第二步:有序序列为{5,6}。 第三个数3开始进行插入排序。由于5,6均大于3,因此数字5、6需要往后挪一个位置。然后再将3放到第一个位置。得到有序序列{3,5,6}。 第三步:有序序列为{3,5,6}。 第四个数1开始进行插入排序。由于3,5,6均大于1,因此数字3、5、6需要往后挪一个位置。然后再将1放到第一个位置。得到有序序列{1,3,5,6}。 第四步:有序序列为{1,3,5,6}。 第五个数8开始进行插入排序。由于1、3、5、6都是小于8的,所以位置不用改动。在最后一个位置插入数字8,得到有序序列{1,3,5,6,8}。 第五步:有序序列为{1,3,5,6,8}。 第七个数7开始进行插入排序。因为1、3、5、6都是小于7的,所以位置不用改动,由于8大于7,因此往后挪一个位置,然后在6和8之间插入数字7。得到有序序列{1,3,5,6,7,8}。 至此,整个插入排序过程完成。
#include <iostream>
using namespace std;
int a[10005];
int main()
{
int n;
int key,j;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=2;i<=n;i++){//从第二个开始排序
key=a[i]; //待记录插入的数字
j=i-1;//令j=已有序列的尾位置
while(j>=1&&key<a[j]) //从后往前遍历序列已有序列,直到第一个比key小的位置
{
a[j+1]=a[j];//当前元素比关键字大,则往前插空
j--;
}
a[j+1]=key;//直到无法前移的时候,将key插入空出的位置
}
for(int i=1;i<=n;i++) cout<<a[i]<<' ';
return 0;
}
计数排序(Counting sort)是一种稳定的线性时间排序算法。是桶排序的特殊情况,只需要划分,不需要进行桶排序的第二步的排序操作。 像快排、堆排、归并等排序算法都是基于比较的排序算法,时间复杂度最好情况也只能降到O(nlogn)。 计数排序是一种线性排序算法,不需要进行比较,时间复杂度为O(n)。(注意是计数排序不是基数排序,两者不同) 基本思想是:对于每个元素x,找出比x小的数的个数,从而确定x在排好序的数组中的位置。此算法需要辅助数组,是以空间换时间。
计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),然后进行分配、收集处理: ① 分配。扫描一遍原始数组,以当前值-minValue作为下标,将该下标的计数器增1。 ② 收集。扫描一遍计数器数组,按顺序把值收集起来。
① 找出待排序的数组中最大和最小的元素 ② 统计数组中每个值为i的元素出现的次数,存入数组C的第i项 ③ 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加) ④ 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
举个例子 假设有无序数列nums=[2, 1, 3, 1, 5], 首先扫描一遍获取最小值和最大值,maxValue=5, minValue=1,于是开一个长度为5的计数器数组counter (1) 分配
统计每个元素出现的频率,得到counter=[2, 1, 1, 0, 1],例如counter[0]表示值0+minValue=1出现了2次。
(2) 收集
counter[0]=2表示1出现了两次,那就向原始数组写入两个1,counter[1]=1表示2出现了1次,那就向原始数组写入一个2,依次类推,最终原始数组变为[1,1,2,3,5],排序好了。
平均时间复杂度:O(n + k) 最佳时间复杂度:O(n + k) 最差时间复杂度:O(n + k) 空间复杂度:O(n + k)
当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。。在实际工作中,当k=O(n)时,我们一般会采用计数排序,这时的运行时间为O(n)。
计数排序需要两个额外的数组用来对元素进行计数和保存排序的输出结果,所以空间复杂度为O(k+n)。
计数排序的一个重要性质是它是稳定的:具有相同值的元素在输出数组中的相对次序与它们在输入数组中的相对次序是相同的。也就是说,对两个相同的数来说,在输入数组中先出现的数,在输出数组中也位于前面。
计数排序的稳定性很重要的一个原因是:计数排序经常会被用于基数排序算法的一个子过程。我们将在后面文章中介绍,为了使基数排序能够正确运行,计数排序必须是稳定的。
统计关键字的个数,放入数组C-图1
对C进行累加处理,为了找对应A的排好序的位置-图2
详细操作过程
#include<string.h>
#include<iostream>
// 任何比较排序算法的时间复杂度的上限为O(NlogN), 不存在比o(nlgN)更少的比较排序算法。
// 如果想要在时间复杂度上超过O(NlogN)的时间复杂度,肯定需要加入其它条件。计数排序就加入
// 了限制条件,从而使时间复杂度为O(N).
//
// 计数排序的核心思想(来自算法导论):
// 计数排序要求待排序的n个元素的大小在[0, k]之间,并且k与n在一个数量级上,即k=O(n).
// 对于每一个输入元素x, 确定小于等于x的个数为i。利用这一信息,就可以把元素x放到输出数组
// 的正确位置,即把元素x放到输出数组下标为i-1的位置。
//
// 重要说明:
// 1. 计数排序要求待排序的n个元素的大小在[0, k]之间,并且k与n在一个数量级上,即k=O(n).
// 此时使用计数排序可以把时间复杂度降到O(n)上。
// 2. 计数排序不是基于比较的排序算法,它基于计数策略。
// 3. 写计数排序算法时,应该把它写成稳定排序的。
// 4. 计数排序还是原址排序,它需要借助额外的内存空间。
//
// 计数排序代码如下:
// 参数说明:array表示数组指针,nLength_表示数组的最大长度,nMaxNumber_表示数组元素中的最大> 值;
void CountingSort(int array[], int nLength_, int nMaxNumber_)
{
// 参数的合法化检测
if (nullptr == array || nLength_ <= 1 || nMaxNumber_ <= 0)
return;
// 统计待排序数组中每一个元素的个数
// 注意:此处new出来的数组的大小为nMaxNumber_ + 1, 用于统计[0, nMaxNumber_]范围内的元素
int* ArrayCount = new int[nMaxNumber_ + 1]{0};
for (int i = 0; i < nLength_; ++i)
{
++ArrayCount[array[i]];
}
// 此处计算待排序数组中小于等于第i个元素的个数.
// 备注:如果要进行大到小的排序,就计算大于等于第i个元素的个数, 也就从后向前进行累加;
for (int i = 1; i < nMaxNumber_ + 1; ++i)
{
ArrayCount[i] += ArrayCount[i-1];
}
// 把待排序的数组放到输出数组中, 为了保持排序的稳定性,从后向前添加元素
int* ArrayResult = new int[nLength_];
for (int i = nLength_ - 1; i >=0; --i)
{
int _nIndex = ArrayCount[array[i]] - 1; // 元素array[i]在输出数组中的下标
ArrayResult[_nIndex] = array[i];
// 因为可能有重复的元素,所以要减1,为下一个重复的元素计算正确的下标;
--ArrayCount[array[i]];
}
// 交换数据并释放内存空间
memcpy(array, ArrayResult, sizeof(int) * nLength_);
delete [] ArrayCount;
ArrayCount = nullptr;
delete [] ArrayResult;
ArrayResult = nullptr;
}
// 测试代码
/*************** main.c *********************/
static void PrintArray(int array[], int nLength_);
int main(int argc, char* argv[])
{
int test[10] = {12, 12, 4, 0, 8, 5, 2, 3, 9, 8};
std::cout << "排序前:" << std::endl;
PrintArray(test, 10);
CountingSort(test, 10, 12);
std::cout << "排序后:" << std::endl;
PrintArray(test, 10);
return 0;
}
// 打印数组函数
static void PrintArray(int array[], int nLength_)
{
if (nullptr == array || nLength_ <= 0)
return;
for (int i = 0; i < nLength_; ++i)
{
std::cout << array[i] << " ";
}
std::cout << std::endl;
}
#include <iostream>
#include <vector>
using namespace std;
void CountSort(vector<int> &arr, int maxVal) {
int len = arr.size();
if (len < 1)
return;
vector<int> count(maxVal+1, 0);
vector<int> tmp(arr);
for (auto x : arr)
count[x]++;
for (int i = 1; i <= maxVal; ++i)
count[i] += count[i - 1];
for (int i = len - 1; i >= 0; --i) {
arr[count[tmp[i]] - 1] = tmp[i];
count[tmp[i]]--; //注意这里要减1
}
}
int main()
{
vector<int> arr = { 1,5,3,7,6,2,8,9,4,3,3 };
int maxVal = 9;
CountSort(arr,maxVal);
for (auto x : arr)
cout << x << " ";
cout << endl;
return 0;
}
参考:https://blog.csdn.net/m0_64036070/article/details/123826962 https://blog.csdn.net/Passerby_XX/article/details/123147928 桶排序是计数排序的升级版,也是分治算法。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。简言之,将值为i的元素放入i号桶,最后依次把桶里的元素倒出来。
设置一个定量的数组当作空桶子。 寻访序列,并且把项目一个一个放到对应的桶子去。 对每个不是空的桶子进行排序。 从不是空的桶子里把项目再放回原来的序列中
桶排序算法中,待排序的数据量和桶的数量并不一定是简单的“一对一”的关系,更多场景中是“多对一”的关系,
我们可以利用桶来完成
去重
与计数
的任务。 解决去重问题时,只需将每个数据装入桶中后,再根据桶中是否有数据( tong[i]>0),来输出对应的桶的编号,只输出1次而不要多次输出。 解决计数问题的时候,我们只需要输出桶中的数据即为元素出现的次数。 所以,桶排序其实类似于计数,利用了桶的编号天然有序的性质,过程类似于“唱票”。
#include <bits/stdc++.h>
using namespace std;
int n; //要给n个数排序
int bucket[1005]; //满足所有数不超过 bucket[x]的x
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int a;
cin>>a;
bucket[a]++; //进入相应的区域(桶)
//如果题目要求把重复的数删掉(去重)
//可以写 bucket[a]=1;
}
for(int i=0;i<=1005;i++) //要把所有区域里的数“拽”出来
while(bucket[i]>0)
{
cout<<i<<' '; //i指的是数
bucket[i]--; //bucket[i]指的是区域 i里剩余数的数量
}
return 0;
}
【描述】有n个正整数,数字范围在1~10000之间,请你将这n个数字从小到大输出。n<=100000000。 【输入】输入共2行,第1行是n。第2行是n个整数. 【输出】1行。所有整数从低到高排序后的结果。 【样例输入】 10 1 2 3 4 6 5 9 8 7 0 【输出】 0 1 2 3 4 5 6 7 8 9
#include <bits/stdc++.h>
using namespace std;
int n; //要给n个数排序
int bucket[10005]; //满足所有数不超过 bucket[x]的x
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int a;
cin>>a;
bucket[a]++; //进入相应的区域(桶)
//如果题目要求把重复的数删掉(去重)
//可以写 bucket[a]=1;
}
for(int i=0;i<=10000;i++) //要把所有区域里的数“拽”出来
// while(bucket[i]>0)
// {
// cout<<i<<' '; //i指的是数
// bucket[i]--; //bucket[i]指的是区域 i里剩余数的数量
// }
for(int j=1;j<=bucket[i];j++)
{
cout<<i<<" ";
}
return 0;
}
【描述】有n个正整数,数字范围在-1000~1000之间,请你将这n个数字从小到大输出。n<=100000000。 【输入】输入共2行,第1行是n。第2行是n个整数. 【输出】1行。所有整数从高到低排序后的结果。 【样例输入】 6 11 22 33 -44 -32 1 【输出】 33 22 11 1 -32 -44
#include <bits/stdc++.h>
using namespace std;
int n; //要给n个数排序
int bucket[2001]; //满足所有数不超过 bucket[x]的x.-1000 1000 变化为0-2000
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int a;
cin>>a;
bucket[a+1000]++; //进入相应的区域(桶)
//如果题目要求把重复的数删掉(去重)
//可以写 bucket[a]=1;
}
for(int i=2000;i>=0;i--) //要把所有区域里的数“拽”出来
for(int j=1;j<=bucket[i];j++)
{
cout<<i-1000<<" "; //减去1000
}
return 0;
}
【描述】小明所在的学校近期统计了师生的年龄数据,教务处主任希望你帮忙把这些年龄按从小到大的顺序排序,重复的年龄只保留一个。作为编程小高手的你能帮他解决这个问题吗? 【输入】输入共2行,第1行是n。第2行是n个整数. 【输出】1行。所有整数去重后从低到高排序后的结果。 【样例输入】 6 11 12 12 10 11 13 【输出】 10 11 12 13
#include <bits/stdc++.h>
using namespace std;
int n; //要给n个数排序
int bucket[105]; //满足所有数不超过 bucket[x]的x.-1000 1000 变化为0-2000
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int a;
cin>>a;
bucket[a]++; //进入相应的区域(桶)
//如果题目要求把重复的数删掉(去重)
//可以写 bucket[a]=1;
}
for(int i=1;i<=100;i++) //要把所有区域里的数“拽”出来
if(bucket[i]) cout<<i<<" ";
return 0;
}
【描述】北京市中小学刚刚结束期中测试,信息学老师想统计全市学生信息学成绩的得分情况,即某些分数的人数,以便改进教学内容和方法,提高同学们的信息学成绩,同学们写个程序,帮助老师实现吧。【输入】输入共3行,第1行是n和k。第2行是n个整数,第3行是k个得分; 【输出】1行k个整数,分别为k个得分的人数; 【样例输入】 6 2 11 12 12 10 11 13 12 13 【输出】 2 1
#include <bits/stdc++.h>
using namespace std;
int n,k,score; //要给n个数排序
int bucket[105]; //满足所有数不超过 bucket[x]的x.-1000 1000 变化为0-2000
int main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)
{
int a;
cin>>a;
bucket[a]++; //进入相应的区域(桶)
//如果题目要求把重复的数删掉(去重)
//可以写 bucket[a]=1;
}
for(int i=1;i<=k;i++)
{
cin>>score;
cout<<bucket[score]<<" ";
}
return 0;
}
【题目描述】 一个
个元素的整数数组,如果数组两个连续元素之间差的绝对值包括了
之间的所有整数,则称之符合“欢乐的跳”,如数组
符合“欢乐的跳”,因为差的绝对值分别为:
。
给定一个数组,你的任务是判断该数组是否符合“欢乐的跳”。
【输入格式】
每组测试数据第一行以一个整数
开始,接下来
个空格隔开的在
之间的整数。
【输出格式】
对于每组测试数据,输出一行若该数组符合“欢乐的跳”则输出 Jolly
,否则输出 Not jolly
。
【样例 #1】
【样例输入 #1】
4 1 4 2 3
【样例输出 #1】
Jolly
【样例 #2】
【样例输入 #2】
5 1 4 2 -1 6
【样例输出 #2】
Not jolly
【 提示】
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a[1100],b[1100],n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
for(int j=1;j<=n-1;j++)
{
b[j]=abs(a[j]-a[j+1]);
}
sort(b+1,b+1+n-1);
int s =0;
for(int k=1;k<=n-1;k++)
{
if(b[k]==k)s++;
}
if(s==n-1)cout<<"Jolly";
else cout<<"Not jolly";
return 0;
}
【 题目描述】 设有
个正整数
,将它们联接成一排,相邻数字首尾相接,组成一个最大的整数。 【 输入格式】 第一行有一个整数,表示数字个数
。 第二行有
个整数,表示给出的
个整数
。 【 输出格式】 一个正整数,表示最大的整数
【 样例 】1 【 样例输入 】1
3
13 312 343
【 样例输出 】1
34331213
【 样例 】2
【 样例输入 】2
4
7 13 4 246
【 样例输出 】2
7424613
【 提示】
对于全部的测试点,保证
,
。
#include <bits/stdc++.h>
using namespace std;
bool cmp(string a,string b){
return a+b>b+a;
}
int main()
{
string a[22];
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+1+n.cmp);
for(int i=1;i<=n;i++) cout<<a[i];
return 0;
}
#include <bits/stdc++.h>
using namespace std;
bool cmp(string a,string b){
return a+b>b+a;
}
int main()
{
string a[22],key;
int n,j;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=2;i<=n;i++)
{
key =a[i];
for(j=i-1;j>=1;j--)
{
if(key+a[j]>a[j]+key) a[j+1]=a[j];//key要插入到a[j]前
else break;
}
a[j+1]=key;
}
for(int i=1;i<=n;i++) cout<<a[i];
return 0;
}