【问题标题】:PHP custom sorting: Manual sorting an array based on specified keysPHP自定义排序:根据指定键手动排序数组
【发布时间】:2020-05-30 09:28:45
【问题描述】:

我有一个看起来像的数组

$array = [
    //...
    'name' => ['value' => 'Raj KB'],
    'street' => ['value' => 'Street ABC'],
    'city' => ['value' => 'Dubai'],
    'country_id' => ['value' => 'UAE'],
    'region' => ['value' => 'DXB'],
    'region_id' => ['value' => 11],
    'zip_code' => ['value' => 12345],
    'city_id' => ['value' => 22],
    //...
];

我想对数组进行排序,以便键 country_idregionregion_idcitycity_id 连续出现,同时保留其他键的位置。

预期输出

 $array = [
    //...
    'name' => ['value' => 'Raj KB'],
    'street' => ['value' => 'Street ABC'],
    'country_id' => ['value' => 'UAE'],
    'region' => ['value' => 'DXB'],
    'region_id' => ['value' => 11],
    'city' => ['value' => 'Dubai'],
    'city_id' => ['value' => 22],
    'zip_code' => ['value' => 12345],
    //...
];

我尝试过:

试验#1

uksort($array, function ($a, $b) {

  $order = ['country_id' => 0, 'region' => 1, 'region_id' => 2, 'city' => 3, 'city_id' => 4];
  if (isset($order[$a]) && isset($order[$b])) {
    return $order[$a] - $order[$b];
  } else {
    return 0;
  }
});

var_dump($array);

试验#2

uksort($array, function ($a, $b) {

  $order = ['country_id' => 0, 'region' => 1, 'region_id' => 2, 'city' => 3, 'city_id' => 4];
  if (!isset($order[$a]) && !isset($order[$b])) {
    return 0;
  } elseif (!isset($order[$a])) {
    return 1;
  } elseif (!isset($order[$b])) {
    return -1;
  } else {
    return $order[$a] - $order[$b];

  }
});

var_dump($array);

但其余订单不再维护。 所以我希望这些自定义字段以相同的顺序出现,而不会破坏其他人的位置。例如,name 应该首先出现,等等。

【问题讨论】:

  • uksort() 会做我猜的工作
  • 您为什么期望name 早点出现,而zip_code 晚点出现?您是否期望原始订单以某种方式发挥作用?我没有看到逻辑。
  • 我只希望那些顺序整理,其他的就这样吧。
  • 但这并没有很好的定义。哪个元素(也出现在$orders 中)应该为其他匹配项定义锚点来组装?例如,为什么不是所有匹配项都移动到city_id 之前的位置,所以它们与它相邻,所以zip_code 会出现在第三位?您的示例非常简单,但如果匹配分布在一个非常大的数组中,并且完全无序,则可能会变得更加不清楚。比赛应该在哪里集合?

标签: php arrays sorting


【解决方案1】:

看起来你想要的很难用 PHP 的一种排序方法来实现。此外,由于不匹配键的相对顺序不应该改变,我们可以争取比 O(nlogn) 排序方法更好的时间复杂度。

因此,我建议编写一个函数,对两个数组($array$order)进行一些迭代,以便按预期顺序收集键/值对。这构成了 O(n+m) 时间复杂度,其中 nm 是两个数组的两个大小。

函数如下:

function sortadjacent($array, $order) {
    $insertAt = 0;
    foreach($array as $key => $_) {
        if (isset($order[$key])) break;
        $insertAt++;
    }

    $special = [];
    foreach($order as $key => $_) {
        if (isset($array[$key])) $special[$key] = $array[$key];
    }

    $result = [];
    foreach($array as $key => $value) {
        if (!isset($order[$key])) $result[$key] = $value;
        else if (count($result) == $insertAt) $result = array_merge($result, $special);
    }

    return $result;
}

你可以这样称呼它:

$result = sortadjacent($array, $order);

请注意,此函数不会更改 $array,而是在新数组中返回预期结果。

【讨论】:

    【解决方案2】:

    您的实现非常接近,但您必须考虑比较函数中只有一个您想要的键而不存在其他任何键的情况。如果你在这种情况下return 0,它们将在你的数组中的其他键中被破坏(因为在这种情况下它们的位置被认为是相等的)。

    由于您还希望保留现有键的序列,并在country_id 之后插入其他“提取”键,因此您可以保留对原始排序顺序的引用并使用它来解析排序顺序其他字段与country_id的关系(以及其他字段之间以保持当前排序顺序)

    通过处理这两种特殊情况,明确地对您希望出现的键进行单独排序,您将获得满足您要求的结果:

    $order = ['country_id' => 1, 'region' => 2, 'region_id' => 3, 'city' => 4, 'city_id' => 5];
    $preset_order = array_flip(array_keys($array));
    
    uksort($array, function ($a, $b) use ($order, $preset_order) {
      if (isset($order[$a]) && isset($order[$b])) {
        return $order[$a] - $order[$b];
      } else if (isset($order[$a])) {
        return $preset_order['country_id'] - $preset_order[$b];
      } else if (isset($order[$b])) {
        return $preset_order[$a] - $preset_order['country_id'];
      } else {
        return $preset_order[$a] - $preset_order[$b];
      }
    });
    

    输出:

    array(8) {
      'name' =>
      array(1) {
        'value' =>
        string(6) "Raj KB"
      }
      'street' =>
      array(1) {
        'value' =>
        string(10) "Street ABC"
      }
      'country_id' =>
      array(1) {
        'value' =>
        string(3) "UAE"
      }
      'region' =>
      array(1) {
        'value' =>
        string(3) "DXB"
      }
      'region_id' =>
      array(1) {
        'value' =>
        int(11)
      }
      'city' =>
      array(1) {
        'value' =>
        string(5) "Dubai"
      }
      'city_id' =>
      array(1) {
        'value' =>
        int(22)
      }
      'zip_code' =>
      array(1) {
        'value' =>
        int(12345)
      }
    }
    

    【讨论】:

    • 这不是预期的输出。虽然国家、地区、城市是连续出现的,但其余的位置都乱了。
    • 您唯一声明的要求是键顺序出现。在这种情况下,如果剩余键的位置很重要:“提取”的键应该从哪里开始?它们是否应该从任何一个键的第一次出现开始连续定位?可以将它们移到最后,而其余元素保持现有顺序吗?
    • 例如,如果键的顺序是:name、street、city、country_id、region、region_id、zip_code、city_id。它应该出现为:名称、街道、{country_id、region、region_id、city、city_id}、zip_code。你可以看到休息位置被保留了。
    • 是的,但是如果是 name, street, city, region_id, zip_code, city_id, country_id, region 呢?那么它应该从country_id 还是region_id 开始?
    • @trincot 我知道,这就是我问的原因。我不是 OP。
    【解决方案3】:

    PHP 正在使用 Quicksort,因此您不能只为要排序的元素返回有意义的值。 在我看来,在这里使用 uksort 是个坏主意,因为您必须将数组的当前索引用作值,但这是不可能的,因为您无法从比较函数内部访问旧数组的副本。此外,您还需要知道您的第一个特殊值在哪个索引上。

    所以我建议这样做,因为我认为用 uksort 做你想做的事情是不可能的:

    function customSort($array)
    {
        $order = ['country_id' => 0, 'region' => 1, 'region_id' => 2, 'city' => 3, 'city_id' => 4];
        $keyArray = array();
        $sortedArray = array();
        foreach ($array as $i => $value) {
            $keyArray[] = $i;
        }
        $counter = 0;
        $hasStarted = false;
        $insertLater = array();
        for ($i = 0; $i < count($keyArray); $i++) {
            if ($hasStarted) {
                if ($counter < count($order)) {
                    $sortedArray[array_search($counter, $order)] = $array[array_search($counter, $order)];
    
                    $counter++;
                    if (!isset($order[$keyArray[$i]])) {
                        array_push($insertLater, ["key" => $keyArray[$i], "value" => $array[$keyArray[$i]]]);
                    }
                    continue;
                }
            }
    
            if (count($insertLater) > 0) {
                $itemToInsert = array_shift($insertLater);
                $sortedArray[$itemToInsert["key"]] = $itemToInsert["value"];
    
                if (!isset($order[$keyArray[$i]])) {
                    array_push($insertLater, ["key" => $keyArray[$i], "value" => $array[$keyArray[$i]]]);
                }
                continue;
            }
            if (isset($order[$keyArray[$i]]) && !$hasStarted) {
                $sortedArray[array_search($counter, $order)] = $array[array_search($counter, $order)];
                $hasStarted = true;
                $counter++;
                continue;
            }
            $sortedArray[$keyArray[$i]] = $array[$keyArray[$i]];
        }
        return $sortedArray;
    }
    It's may
    

    不是最好的解决方案,但它的工作时间为 O(n)。

    【讨论】:

    • 这不可能。当您在第一次迭代中进入else 块时,在第二次迭代中进入if 时,那么第二次迭代将覆盖第一次迭代添加到已排序的数据数组,因为密钥完全相同($count == 0)。
    • 哦,有一个错误。我现在修好了
    • 你测试过这个吗?这现在遇到了异常。
    • 我没有例外,但是我误解了有序元素的出现,所以我现在也改了。
    【解决方案4】:

    一个想法,构建一个生成器以您想要的顺序生成键(根据我对问题的理解,有序键组在第一个有序键出现时开始):

    $ordered_keys = ['country_id', 'region', 'region_id', 'city', 'city_id'];
    
    $keys = (function ($array, $ordered_keys) {
        $flag = true;
        foreach ($array as $key => $v) {
            if ( $flag && in_array($key, $ordered_keys) ) {
                yield from $ordered_keys;
                $flag = false;
            }
            yield $key;
        }
    })($array, $ordered_keys);
    
    $result = [];
    
    foreach($keys as $key) {
        $result[$key] = $array[$key];
    }
    
    print_r($result);
    

    demo

    请注意,某些有序键的值将被设置两次,但这并不重要,因为只有第一个设置决定了键的顺序。


    你甚至不需要生成器,你可以直接在一个循环中完成:

    $ordered_keys = ['country_id', 'region', 'region_id', 'city', 'city_id'];
    
    $flag = true;
    $result = [];
    
    foreach ($array as $k=>$v) {
        if ( $flag && in_array($k, $ordered_keys) ) {
            $flag = false;
            foreach ($ordered_keys as $key) {
                $result[$key] = $array[$key];
            }
        } else {
            $result[$k] = $array[$k];
        }
    }
    
    print_r($result);
    

    demo


    其他想法:构建一个具有相同键但具有整数值的数组。

    在第一个“有序键”之前,值在 0-255 范围内,在值之后在 512-inf 范围内。已排序键的值已设置在 256-511 范围内。 (这些范围完全是任意的,你可以选择你想要的范围。)

    $ordered_keys = ['country_id' => 256, 'region' => 257, 'region_id' => 258, 'city' => 259, 'city_id' => 260];
    
    $index = 0;
    
    $result = [];
    
    foreach ($array as $k => $v) {
        if ( isset($ordered_keys[$k]) ) {
            $result[$k] = $ordered_keys[$k];
            if ( $index < 255 ) $index = 512;
        } else {
            $result[$k] = $index++;
        }
    }
    
    asort($result);
    
    foreach($result as $k => $v) {
        $result[$k] = $array[$k];
    }
    
    print_r($result);
    

    demo

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多